diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index dc3ba51cb..04d0cd693 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2351,6 +2351,8 @@ Tap the + to start adding people."; "poll_timeline_decryption_error" = "Due to decryption errors, some votes may not be counted"; +"poll_timeline_ended_text" = "Ended the poll"; + // MARK: - Location sharing "location_sharing_title" = "Location"; diff --git a/Riot/Categories/MXEvent.swift b/Riot/Categories/MXEvent.swift index 5b1a85263..f0d10b3da 100644 --- a/Riot/Categories/MXEvent.swift +++ b/Riot/Categories/MXEvent.swift @@ -46,4 +46,14 @@ extension MXEvent { return self } } + + @objc + var isTimelinePollEvent: Bool { + switch eventType { + case .pollStart, .pollEnd: + return true + default: + return false + } + } } diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 05460082a..1dde36ec4 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4843,6 +4843,10 @@ public class VectorL10n: NSObject { public static var pollTimelineDecryptionError: String { return VectorL10n.tr("Vector", "poll_timeline_decryption_error") } + /// Ended the poll + public static var pollTimelineEndedText: String { + return VectorL10n.tr("Vector", "poll_timeline_ended_text") + } /// Please try again public static var pollTimelineNotClosedSubtitle: String { return VectorL10n.tr("Vector", "poll_timeline_not_closed_subtitle") diff --git a/Riot/Modules/MatrixKit/Models/MXKAppSettings.m b/Riot/Modules/MatrixKit/Models/MXKAppSettings.m index 40cf2ebd3..ed523160e 100644 --- a/Riot/Modules/MatrixKit/Models/MXKAppSettings.m +++ b/Riot/Modules/MatrixKit/Models/MXKAppSettings.m @@ -148,6 +148,8 @@ static NSString *const kMXAppGroupID = @"group.org.matrix"; kMXEventTypeStringKeyVerificationDone, kMXEventTypeStringPollStart, kMXEventTypeStringPollStartMSC3381, + kMXEventTypeStringPollEnd, + kMXEventTypeStringPollEndMSC3381, kMXEventTypeStringBeaconInfo, kMXEventTypeStringBeaconInfoMSC3672 ].mutableCopy; @@ -181,6 +183,8 @@ static NSString *const kMXAppGroupID = @"group.org.matrix"; kMXEventTypeStringKeyVerificationDone, kMXEventTypeStringPollStart, kMXEventTypeStringPollStartMSC3381, + kMXEventTypeStringPollEnd, + kMXEventTypeStringPollEndMSC3381, kMXEventTypeStringBeaconInfo, kMXEventTypeStringBeaconInfoMSC3672 ].mutableCopy; diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m index 14684ac5d..d8f55bf14 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m @@ -2881,7 +2881,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { return NO; } - if (event.eventType == MXEventTypePollStart) { + if (event.isTimelinePollEvent) { return YES; } diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m index eaeaa269a..21be58eb1 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m @@ -1606,6 +1606,23 @@ static NSString *const kHTMLATagRegexPattern = @"( } break; } + case MXEventTypePollEnd: + { + if (event.isEditEvent) + { + return nil; + } + + MXEvent* pollStartedEvent = [self->mxSession.store eventWithEventId:event.relatesTo.eventId inRoom:event.roomId]; + + if (pollStartedEvent) { + displayText = [MXEventContentPollStart modelFromJSON:pollStartedEvent.content].question; + } else { + displayText = [VectorL10n pollTimelineEndedText]; + } + + break; + } case MXEventTypePollStart: { if (event.isEditEvent) diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index f3c7db526..219f67481 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -152,6 +152,7 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat break; } case MXEventTypePollStart: + case MXEventTypePollEnd: { self.tag = RoomBubbleCellDataTagPoll; self.collapsable = NO; @@ -635,7 +636,7 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat __block NSInteger firstVisibleComponentIndex = NSNotFound; MXEvent *firstEvent = self.events.firstObject; - BOOL isPoll = (firstEvent.eventType == MXEventTypePollStart); + BOOL isPoll = firstEvent.isTimelinePollEvent; BOOL isVoiceBroadcast = (firstEvent.eventType == MXEventTypeCustom && [firstEvent.type isEqualToString: VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType]); if ((isPoll || self.attachment || isVoiceBroadcast) && self.bubbleComponents.count) @@ -1193,6 +1194,7 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat shouldAddEvent = NO; break; case MXEventTypePollStart: + case MXEventTypePollEnd: shouldAddEvent = NO; break; case MXEventTypeBeaconInfo: diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 873ddab2f..ca6daf5e0 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -3974,7 +3974,7 @@ static CGSize kThreadListBarButtonItemImageSize; }]]; } - if (!isJitsiCallEvent && selectedEvent.eventType != MXEventTypePollStart && + if (!isJitsiCallEvent && !selectedEvent.isTimelinePollEvent && selectedEvent.eventType != MXEventTypeBeaconInfo) { [self.eventMenuBuilder addItemWithType:EventMenuItemTypeQuote @@ -3995,7 +3995,7 @@ static CGSize kThreadListBarButtonItemImageSize; } if (selectedEvent.sentState == MXEventSentStateSent && - selectedEvent.eventType != MXEventTypePollStart && + !selectedEvent.isTimelinePollEvent && // Forwarding of live-location shares still to be implemented selectedEvent.eventType != MXEventTypeBeaconInfo) { @@ -4011,7 +4011,7 @@ static CGSize kThreadListBarButtonItemImageSize; }]]; } - if (!isJitsiCallEvent && BuildSettings.messageDetailsAllowShare && selectedEvent.eventType != MXEventTypePollStart && + if (!isJitsiCallEvent && BuildSettings.messageDetailsAllowShare && !selectedEvent.isTimelinePollEvent && selectedEvent.eventType != MXEventTypeBeaconInfo) { [self.eventMenuBuilder addItemWithType:EventMenuItemTypeShare @@ -7150,6 +7150,7 @@ static CGSize kThreadListBarButtonItemImageSize; case MXEventTypeKeyVerificationDone: case MXEventTypeKeyVerificationCancel: case MXEventTypePollStart: + case MXEventTypePollEnd: case MXEventTypeBeaconInfo: result = NO; break; diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Poll/PollPlainCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Poll/PollPlainCell.swift index 70cf4370f..13ed8711d 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Poll/PollPlainCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Poll/PollPlainCell.swift @@ -23,11 +23,13 @@ class PollPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable, RoomCell override func render(_ cellData: MXKCellData!) { super.render(cellData) - guard let contentView = roomCellContentView?.innerContentView, - let bubbleData = cellData as? RoomBubbleCellData, - let event = bubbleData.events.last, - event.eventType == __MXEventType.pollStart, - let controller = TimelinePollProvider.shared.buildTimelinePollVCForEvent(event) else { + guard + let contentView = roomCellContentView?.innerContentView, + let bubbleData = cellData as? RoomBubbleCellData, + let event = bubbleData.events.last, + event.isTimelinePollEvent, + let controller = TimelinePollProvider.shared.buildTimelinePollVCForEvent(event) + else { return } diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index 977a71e82..f9779641f 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -551,7 +551,7 @@ class NotificationService: UNNotificationServiceExtension { // Otherwise show a generic reaction. notificationBody = NotificationService.localizedString(forKey: "GENERIC_REACTION_FROM_USER", eventSenderName) } - + case .custom: if (event.type == kWidgetMatrixEventTypeString || event.type == kWidgetModularEventTypeString), let type = event.content?["type"] as? String, @@ -568,14 +568,19 @@ class NotificationService: UNNotificationServiceExtension { additionalUserInfo = [Constants.userInfoKeyPresentNotificationOnForeground: true] } } + case .pollStart: notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName) notificationBody = MXEventContentPollStart(fromJSON: event.content)?.question + + case .pollEnd: + notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName) + notificationBody = VectorL10n.pollTimelineEndedText + default: break } - self.validateNotificationContentAndComplete( notificationTitle: notificationTitle, notificationBody: notificationBody, diff --git a/RiotNSE/target.yml b/RiotNSE/target.yml index 08515998b..415f8447a 100644 --- a/RiotNSE/target.yml +++ b/RiotNSE/target.yml @@ -54,6 +54,7 @@ targets: - path: ../Riot/Managers/Locale/LocaleProviderType.swift - path: ../Riot/Managers/EncryptionKeyManager/EncryptionKeyManager.swift - path: ../Riot/Categories/Bundle.swift + - path: ../Riot/Categories/MXEvent.swift - path: ../Riot/Generated/Strings.swift - path: ../Riot/Generated/Images.swift - path: ../Riot/Managers/KeyValueStorage/KeychainStore.swift diff --git a/RiotShareExtension/target.yml b/RiotShareExtension/target.yml index d89575738..2d398950a 100644 --- a/RiotShareExtension/target.yml +++ b/RiotShareExtension/target.yml @@ -42,6 +42,7 @@ targets: - path: . - path: ../Riot/Modules/Common/SegmentedViewController/SegmentedViewController.m - path: ../Riot/Categories/Bundle.swift + - path: ../Riot/Categories/MXEvent.swift - path: ../Riot/Managers/Theme/ - path: ../Riot/Utils/AvatarGenerator.m - path: ../Config/BuildSettings.swift diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift index 3520588e2..c3c1cf327 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift @@ -21,7 +21,7 @@ import SwiftUI struct TimelinePollCoordinatorParameters { let session: MXSession let room: MXRoom - let pollStartEvent: MXEvent + let pollEvent: MXEvent } final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDelegate { @@ -46,7 +46,7 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel init(parameters: TimelinePollCoordinatorParameters) throws { self.parameters = parameters - try pollAggregator = PollAggregator(session: parameters.session, room: parameters.room, pollStartEventId: parameters.pollStartEvent.eventId) + try pollAggregator = PollAggregator(session: parameters.session, room: parameters.room, pollEvent: parameters.pollEvent) pollAggregator.delegate = self viewModel = TimelinePollViewModel(timelinePollDetails: buildTimelinePollFrom(pollAggregator.poll)) @@ -65,7 +65,7 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel .sink { [weak self] identifiers in guard let self = self else { return } - self.parameters.room.sendPollResponse(for: parameters.pollStartEvent, + self.parameters.room.sendPollResponse(for: parameters.pollEvent, withAnswerIdentifiers: identifiers, threadId: nil, localEcho: nil, success: nil) { [weak self] error in @@ -96,7 +96,7 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel } func endPoll() { - parameters.room.sendPollEnd(for: parameters.pollStartEvent, threadId: nil, localEcho: nil, success: nil) { [weak self] _ in + parameters.room.sendPollEnd(for: parameters.pollEvent, threadId: nil, localEcho: nil, success: nil) { [weak self] _ in self?.viewModel.showClosingFailure() } } @@ -131,6 +131,7 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel closed: poll.isClosed, totalAnswerCount: poll.totalAnswerCount, type: pollKindToTimelinePollType(poll.kind), + eventType: parameters.pollEvent.eventType == .pollStart ? .started : .ended, maxAllowedSelections: poll.maxAllowedSelections, hasBeenEdited: poll.hasBeenEdited, hasDecryptionError: poll.hasDecryptionError) diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift index a11cb3a2e..8e5a04cd9 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift @@ -43,7 +43,7 @@ class TimelinePollProvider: NSObject { return coordinator.toPresentable() } - let parameters = TimelinePollCoordinatorParameters(session: session, room: room, pollStartEvent: event) + let parameters = TimelinePollCoordinatorParameters(session: session, room: room, pollEvent: event) guard let coordinator = try? TimelinePollCoordinator(parameters: parameters) else { return nil } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift index be73d7f04..cd806da54 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift @@ -34,6 +34,7 @@ class TimelinePollViewModelTests: XCTestCase { closed: false, totalAnswerCount: 3, type: .disclosed, + eventType: .started, maxAllowedSelections: 1, hasBeenEdited: false, hasDecryptionError: false) diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift index fa0d2ac49..3629aae3e 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift @@ -32,6 +32,11 @@ enum TimelinePollType { case undisclosed } +enum TimelinePollEventType { + case started + case ended +} + struct TimelinePollAnswerOption: Identifiable { var id: String var text: String @@ -62,6 +67,7 @@ struct TimelinePollDetails { var closed: Bool var totalAnswerCount: UInt var type: TimelinePollType + var eventType: TimelinePollEventType var maxAllowedSelections: UInt var hasBeenEdited = true var hasDecryptionError: Bool @@ -70,6 +76,7 @@ struct TimelinePollDetails { closed: Bool, totalAnswerCount: UInt, type: TimelinePollType, + eventType: TimelinePollEventType, maxAllowedSelections: UInt, hasBeenEdited: Bool, hasDecryptionError: Bool) { @@ -78,6 +85,7 @@ struct TimelinePollDetails { self.closed = closed self.totalAnswerCount = totalAnswerCount self.type = type + self.eventType = eventType self.maxAllowedSelections = maxAllowedSelections self.hasBeenEdited = hasBeenEdited self.hasDecryptionError = hasDecryptionError @@ -94,6 +102,10 @@ struct TimelinePollDetails { return type == .disclosed && totalAnswerCount > 0 && hasCurrentUserVoted } } + + var representsPollEndedEvent: Bool { + eventType == .ended + } } struct TimelinePollViewState: BindableState { diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift index 5a9a9e0a1..a53a745b8 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift @@ -22,6 +22,7 @@ enum MockTimelinePollScreenState: MockScreenState, CaseIterable { case closedDisclosed case openUndisclosed case closedUndisclosed + case closedPollEnded var screenType: Any.Type { TimelinePollDetails.self @@ -37,6 +38,7 @@ enum MockTimelinePollScreenState: MockScreenState, CaseIterable { closed: self == .closedDisclosed || self == .closedUndisclosed ? true : false, totalAnswerCount: 20, type: self == .closedDisclosed || self == .openDisclosed ? .disclosed : .undisclosed, + eventType: self == .closedPollEnded ? .ended : .started, maxAllowedSelections: 1, hasBeenEdited: false, hasDecryptionError: false) diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift index 04451bd92..85309c31c 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift @@ -151,6 +151,7 @@ struct TimelinePollAnswerOptionButton_Previews: PreviewProvider { closed: closed, totalAnswerCount: 100, type: type, + eventType: .started, maxAllowedSelections: 1, hasBeenEdited: false, hasDecryptionError: false) diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift index cc00f017a..2109a0e8a 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift @@ -31,6 +31,12 @@ struct TimelinePollView: View { let poll = viewModel.viewState.poll VStack(alignment: .leading, spacing: 16.0) { + if poll.representsPollEndedEvent { + Text(VectorL10n.pollTimelineEndedText) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.tertiaryContent) + } + Text(poll.question) .font(theme.fonts.bodySB) .foregroundColor(theme.colors.primaryContent) + diff --git a/SiriIntents/target.yml b/SiriIntents/target.yml index 72bbf91d4..5b63a95b3 100644 --- a/SiriIntents/target.yml +++ b/SiriIntents/target.yml @@ -42,6 +42,7 @@ targets: sources: - path: . - path: ../Riot/Categories/Bundle.swift + - path: ../Riot/Categories/MXEvent.swift - path: ../Config/CommonConfiguration.swift - path: ../Config/BuildSettings.swift - path: ../Config/Configurable.swift diff --git a/changelog.d/pr-7231.change b/changelog.d/pr-7231.change new file mode 100644 index 000000000..288daeae1 --- /dev/null +++ b/changelog.d/pr-7231.change @@ -0,0 +1 @@ +Polls: render the poll ended event in the timeline.