diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index b2f2109fe..ef981baaf 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 56; objects = { /* Begin PBXBuildFile section */ @@ -899,7 +899,7 @@ 127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentValuePublisher.swift; sourceTree = ""; }; 12EDAFB64FA5F6812D54F39A /* MigrationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationScreenViewModel.swift; sourceTree = ""; }; 12F1E7F9C2BE8BB751037826 /* WaitlistScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenCoordinator.swift; sourceTree = ""; }; - 1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; path = IntegrationTests.xctestplan; sourceTree = ""; }; + 1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = IntegrationTests.xctestplan; sourceTree = ""; }; 130ED565A078F7E0B59D9D25 /* UNTextInputNotificationResponse+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNTextInputNotificationResponse+Creator.swift"; sourceTree = ""; }; 13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; 1423AB065857FA546444DB15 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; @@ -1048,7 +1048,7 @@ 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxyProtocol.swift; sourceTree = ""; }; 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineView.swift; sourceTree = ""; }; 47873756E45B46683D97DC32 /* LegalInformationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenModels.swift; sourceTree = ""; }; - 478BE8591BD13E908EF70C0C /* DesignKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = DesignKit; path = DesignKit; sourceTree = SOURCE_ROOT; }; + 478BE8591BD13E908EF70C0C /* DesignKit */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DesignKit; sourceTree = SOURCE_ROOT; }; 4798B3B7A1E8AE3901CEE8C6 /* FramePreferenceKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramePreferenceKey.swift; sourceTree = ""; }; 47EBB5D698CE9A25BB553A2D /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; 47F29139BC2A804CE5E0757E /* MediaUploadPreviewScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModel.swift; sourceTree = ""; }; @@ -1063,7 +1063,7 @@ 4B41FABA2B0AEF4389986495 /* LoginMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginMode.swift; sourceTree = ""; }; 4B5046BB295AEAFA6FB81655 /* SessionVerificationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenModels.swift; sourceTree = ""; }; 4BD371B60E07A5324B9507EF /* AnalyticsSettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenCoordinator.swift; sourceTree = ""; }; - 4CD6AC7546E8D7E5C73CEA48 /* ElementX.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = ElementX.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 4CD6AC7546E8D7E5C73CEA48 /* ElementX.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ElementX.app; sourceTree = BUILT_PRODUCTS_DIR; }; 4CDDDDD9FE1A699D23A5E096 /* LoginScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreen.swift; sourceTree = ""; }; 4D6E4C37E9F0E53D3DF951AC /* HomeScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenUITests.swift; sourceTree = ""; }; 4E2245243369B99216C7D84E /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = ""; }; @@ -1237,7 +1237,7 @@ 8D55702474F279D910D2D162 /* RoomStateEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateEventStringBuilder.swift; sourceTree = ""; }; 8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = ""; }; - 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = ""; }; + 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UITests.xctestplan; sourceTree = ""; }; 8E1BBA73B611EDEEA6E20E05 /* InvitesScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenModels.swift; sourceTree = ""; }; 8EC57A32ABC80D774CC663DB /* SettingsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenUITests.swift; sourceTree = ""; }; 8F21ED7205048668BEB44A38 /* AppActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppActivityView.swift; sourceTree = ""; }; @@ -1349,7 +1349,7 @@ B4CFE236419E830E8946639C /* Analytics+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Analytics+SwiftUI.swift"; sourceTree = ""; }; B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableAvatarImage.swift; sourceTree = ""; }; B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineView.swift; sourceTree = ""; }; - B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; path = ConfettiScene.scn; sourceTree = ""; }; + B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = ConfettiScene.scn; sourceTree = ""; }; B6311F21F911E23BE4DF51B4 /* ReadMarkerRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadMarkerRoomTimelineView.swift; sourceTree = ""; }; B697816AF93DA06EC58C5D70 /* WaitlistScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenViewModelProtocol.swift; sourceTree = ""; }; B6E89E530A8E92EC44301CA1 /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; @@ -1438,7 +1438,7 @@ CD95B3714F806AC9CF9A557B /* ComposerToolbarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbarViewModel.swift; sourceTree = ""; }; CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProvider.swift; sourceTree = ""; }; CEE0E6043EFCF6FD2A341861 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = ""; }; - CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; path = UnitTests.xctestplan; sourceTree = ""; }; + CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UnitTests.xctestplan; sourceTree = ""; }; CF48AF076424DBC1615C74AD /* AuthenticationServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProxy.swift; sourceTree = ""; }; D0140615D2232612C813FD6C /* EncryptedHistoryRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedHistoryRoomTimelineItem.swift; sourceTree = ""; }; D071F86CD47582B9196C9D16 /* UserDiscoverySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoverySection.swift; sourceTree = ""; }; @@ -1518,7 +1518,7 @@ ECF79FB25E2D4BD6F50CE7C9 /* RoomMembersListScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenViewModel.swift; sourceTree = ""; }; ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomCell.swift; sourceTree = ""; }; ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = ""; }; - ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = ""; }; + ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = message.caf; sourceTree = ""; }; ED983D4DCA5AFA6E1ED96099 /* StateRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateRoomTimelineView.swift; sourceTree = ""; }; EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModelTests.swift; sourceTree = ""; }; EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItemContent.swift; sourceTree = ""; }; @@ -1532,7 +1532,7 @@ F174A5627CDB3CAF280D1880 /* EmojiPickerScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenModels.swift; sourceTree = ""; }; F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProvider.swift; sourceTree = ""; }; F1B8500C152BC59445647DA8 /* UnsupportedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsupportedRoomTimelineItem.swift; sourceTree = ""; }; - F2D513D2477B57F90E98EEC0 /* portrait_test_video.mp4 */ = {isa = PBXFileReference; path = portrait_test_video.mp4; sourceTree = ""; }; + F2D513D2477B57F90E98EEC0 /* portrait_test_video.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = portrait_test_video.mp4; sourceTree = ""; }; F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntityRegexTests.swift; sourceTree = ""; }; F348B5F2C12F9D4F4B4D3884 /* VideoRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItem.swift; sourceTree = ""; }; F36C0A6D59717193F49EA986 /* UserSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionTests.swift; sourceTree = ""; }; diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 2a6f2a543..1d1005717 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -22,6 +22,7 @@ "action_done" = "Done"; "action_edit" = "Edit"; "action_enable" = "Enable"; +"action_end_poll" = "End poll"; "action_forgot_password" = "Forgot password?"; "action_forward" = "Forward"; "action_invite" = "Invite"; diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 6b7c896ba..c519a6893 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -56,6 +56,8 @@ public enum L10n { public static var actionEdit: String { return L10n.tr("Localizable", "action_edit") } /// Enable public static var actionEnable: String { return L10n.tr("Localizable", "action_enable") } + /// End poll + public static var actionEndPoll: String { return L10n.tr("Localizable", "action_end_poll") } /// Forgot password? public static var actionForgotPassword: String { return L10n.tr("Localizable", "action_forgot_password") } /// Forward diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 0b3460355..bec7710c0 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -1485,6 +1485,48 @@ class RoomProxyMock: RoomProxyProtocol { return createPollQuestionAnswersPollKindReturnValue } } + //MARK: - sendPollResponse + + var sendPollResponsePollStartIDAnswersCallsCount = 0 + var sendPollResponsePollStartIDAnswersCalled: Bool { + return sendPollResponsePollStartIDAnswersCallsCount > 0 + } + var sendPollResponsePollStartIDAnswersReceivedArguments: (pollStartID: String, answers: [String])? + var sendPollResponsePollStartIDAnswersReceivedInvocations: [(pollStartID: String, answers: [String])] = [] + var sendPollResponsePollStartIDAnswersReturnValue: Result! + var sendPollResponsePollStartIDAnswersClosure: ((String, [String]) async -> Result)? + + func sendPollResponse(pollStartID: String, answers: [String]) async -> Result { + sendPollResponsePollStartIDAnswersCallsCount += 1 + sendPollResponsePollStartIDAnswersReceivedArguments = (pollStartID: pollStartID, answers: answers) + sendPollResponsePollStartIDAnswersReceivedInvocations.append((pollStartID: pollStartID, answers: answers)) + if let sendPollResponsePollStartIDAnswersClosure = sendPollResponsePollStartIDAnswersClosure { + return await sendPollResponsePollStartIDAnswersClosure(pollStartID, answers) + } else { + return sendPollResponsePollStartIDAnswersReturnValue + } + } + //MARK: - endPoll + + var endPollPollStartIDCallsCount = 0 + var endPollPollStartIDCalled: Bool { + return endPollPollStartIDCallsCount > 0 + } + var endPollPollStartIDReceivedPollStartID: String? + var endPollPollStartIDReceivedInvocations: [String] = [] + var endPollPollStartIDReturnValue: Result! + var endPollPollStartIDClosure: ((String) async -> Result)? + + func endPoll(pollStartID: String) async -> Result { + endPollPollStartIDCallsCount += 1 + endPollPollStartIDReceivedPollStartID = pollStartID + endPollPollStartIDReceivedInvocations.append(pollStartID) + if let endPollPollStartIDClosure = endPollPollStartIDClosure { + return await endPollPollStartIDClosure(pollStartID) + } else { + return endPollPollStartIDReturnValue + } + } } class RoomTimelineProviderMock: RoomTimelineProviderProtocol { var updatePublisher: AnyPublisher { diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 0d1c2b780..370dfa9c4 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -60,7 +60,7 @@ enum RoomScreenViewAction { case toggleReaction(key: String, itemID: TimelineItemIdentifier) case sendReadReceiptIfNeeded(TimelineItemIdentifier) case paginateBackwards - case selectedPollOption(poll: Poll, optionID: String) + case selectedPollOption(pollStartID: String, optionID: String) case timelineItemMenu(itemID: TimelineItemIdentifier) case timelineItemMenuAction(itemID: TimelineItemIdentifier, action: TimelineItemMenuAction) diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 50b8545bc..a9d0969f9 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -139,8 +139,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol if state.swiftUITimelineEnabled { renderPendingTimelineItems() } - case .selectedPollOption: - break + case let .selectedPollOption(pollStartID, optionID): + sendPollResponse(pollStartID: pollStartID, optionID: optionID) } } @@ -489,7 +489,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol actionsSubject.send(.composer(action: .removeFocus)) state.bindings.actionMenuInfo = .init(item: eventTimelineItem) } - + + // swiftlint:disable:next cyclomatic_complexity private func timelineItemMenuActionsForItemId(_ itemID: TimelineItemIdentifier) -> TimelineItemMenuActions? { guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID), let item = timelineItem as? EventBasedTimelineItemProtocol else { @@ -510,11 +511,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol return .init(actions: [], debugActions: debugActions) } - var actions: [TimelineItemMenuAction] = [ - .reply - ] + var actions: [TimelineItemMenuAction] = [] if item.isMessage { + actions.append(.reply) actions.append(.forward(itemID: itemID)) } @@ -527,6 +527,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } actions.append(.copyPermalink) + + if canRedactItem(item), let poll = item.pollIfAvailable, !poll.hasEnded, let eventID = itemID.eventID { + actions.append(.endPoll(pollStartID: eventID)) + } if canRedactItem(item) { actions.append(.redact) @@ -610,6 +614,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol actionsSubject.send(.displayReportContent(itemID: itemID, senderID: eventTimelineItem.sender.id)) case .react: showEmojiPicker(for: itemID) + case .endPoll(let pollStartID): + endPoll(pollStartID: pollStartID) } if action.switchToDefaultComposer { @@ -791,6 +797,32 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol state.bindings.reactionSummaryInfo = .init(reactions: eventTimelineItem.properties.reactions, selectedKey: selectedKey) } + + // MARK: - Polls + + private func sendPollResponse(pollStartID: String, optionID: String) { + Task { + let sendPollResponseResult = await roomProxy.sendPollResponse(pollStartID: pollStartID, answers: [optionID]) + switch sendPollResponseResult { + case .success: + break + case .failure: + displayError(.toast(L10n.errorUnknown)) + } + } + } + + private func endPoll(pollStartID: String) { + Task { + let endPollResult = await roomProxy.endPoll(pollStartID: pollStartID) + switch endPollResult { + case .success: + break + case .failure: + displayError(.toast(L10n.errorUnknown)) + } + } + } } private extension RoomProxyProtocol { diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/PollRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/PollRoomTimelineView.swift index 836ef9dd1..ddece8368 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/PollRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/PollRoomTimelineView.swift @@ -29,14 +29,15 @@ struct PollRoomTimelineView: View { ForEach(poll.options, id: \.id) { option in Button { - context.send(viewAction: .selectedPollOption(poll: poll, optionID: option.id)) + guard let eventID else { return } + context.send(viewAction: .selectedPollOption(pollStartID: eventID, optionID: option.id)) } label: { PollOptionView(pollOption: option, showVotes: showVotes, isFinalResult: poll.hasEnded) .foregroundColor(progressBarColor(for: option)) } - .disabled(poll.hasEnded) + .disabled(poll.hasEnded || eventID == nil) } summaryView @@ -51,6 +52,10 @@ struct PollRoomTimelineView: View { timelineItem.poll } + private var eventID: String? { + timelineItem.id.eventID + } + private var questionView: some View { HStack(alignment: .top, spacing: 12) { Image(Asset.Images.timelinePoll.name) @@ -102,10 +107,6 @@ private extension Poll { return L10n.commonPollUndisclosedText } } - - var hasEnded: Bool { - endDate != nil - } } struct PollRoomTimelineView_Previews: PreviewProvider { diff --git a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMenu.swift b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMenu.swift index 41d5d244d..f786f4e74 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMenu.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMenu.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import Compound import SwiftUI struct TimelineItemMenuActions { @@ -51,6 +52,7 @@ enum TimelineItemMenuAction: Identifiable, Hashable { case retryDecryption(sessionID: String) case report case react + case endPoll(pollStartID: String) var id: Self { self } @@ -107,6 +109,7 @@ enum TimelineItemMenuAction: Identifiable, Hashable { case .retryDecryption: return Label(L10n.actionRetryDecryption, systemImage: "arrow.down.message") case .report: return Label(L10n.actionReportContent, systemImage: "exclamationmark.bubble") case .react: return Label(L10n.actionReact, systemImage: "hand.thumbsup") + case .endPoll: return Label { Text(L10n.actionEndPoll) } icon: { Image.compound.check.resizable() } } } } diff --git a/ElementX/Sources/Services/Room/RoomProxy.swift b/ElementX/Sources/Services/Room/RoomProxy.swift index 788b827ee..4cbb9fffc 100644 --- a/ElementX/Sources/Services/Room/RoomProxy.swift +++ b/ElementX/Sources/Services/Room/RoomProxy.swift @@ -688,6 +688,8 @@ class RoomProxy: RoomProxyProtocol { } } + // MARK: - Polls + func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result { await Task.dispatch(on: .global()) { do { @@ -699,6 +701,28 @@ class RoomProxy: RoomProxyProtocol { } } + func sendPollResponse(pollStartID: String, answers: [String]) async -> Result { + await Task.dispatch(on: .global()) { + do { + return try .success(self.room.sendPollResponse(pollStartId: pollStartID, answers: answers, txnId: genTransactionId())) + } catch { + MXLog.error("Failed sending a poll vote: \(error), pollStartID: \(pollStartID)") + return .failure(.failedSendingPollResponse) + } + } + } + + func endPoll(pollStartID: String) async -> Result { + await Task.dispatch(on: .global()) { + do { + return try .success(self.room.endPoll(pollStartId: pollStartID, text: .init(), txnId: genTransactionId())) + } catch { + MXLog.error("Failed ending a poll: \(error), pollStartID: \(pollStartID)") + return .failure(.failedEndingPoll) + } + } + } + // MARK: - Private /// Force the timeline to load member details so it can populate sender profiles whenever we add a timeline listener diff --git a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift index 453582228..b1b2aaf8d 100644 --- a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift @@ -43,6 +43,8 @@ enum RoomProxyError: Error, Equatable { case failedUploadingAvatar case failedCheckingPermission case failedCreatingPoll + case failedSendingPollResponse + case failedEndingPoll } // sourcery: AutoMockable @@ -168,6 +170,10 @@ protocol RoomProxyProtocol { func canUserRedact(userID: String) async -> Result func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result + + func sendPollResponse(pollStartID: String, answers: [String]) async -> Result + + func endPoll(pollStartID: String) async -> Result } extension RoomProxyProtocol { diff --git a/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift b/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift index 93fe15165..3501b62ed 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift @@ -26,7 +26,7 @@ struct TimelineItemIdentifier: Hashable { /// Only available for EventTimelineItem and only when the item is returned by the server. var eventID: String? - /// Uniquely identfies the local echo of the timeline item. + /// Uniquely identifies the local echo of the timeline item. /// Only available for sent EventTimelineItem that have not been returned by the server yet. var transactionID: String? } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift index 2ac2106a3..82284a24d 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift @@ -46,6 +46,10 @@ extension EventBasedTimelineItemProtocol { self is LocationRoomTimelineItem } + var pollIfAvailable: Poll? { + (self as? PollRoomTimelineItem)?.poll + } + var isRedacted: Bool { self is RedactedRoomTimelineItem } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/PollRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/PollRoomTimelineItem.swift index 9af2f2573..6a5af2a74 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/PollRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/PollRoomTimelineItem.swift @@ -35,6 +35,10 @@ struct Poll: Equatable { let votes: [String: [String]] let endDate: Date? + var hasEnded: Bool { + endDate != nil + } + enum Kind: Equatable { case disclosed case undisclosed diff --git a/UITests/Sources/CreatePollScreenUITests.swift b/UITests/Sources/CreatePollScreenUITests.swift index 6ba374e37..c6be49067 100644 --- a/UITests/Sources/CreatePollScreenUITests.swift +++ b/UITests/Sources/CreatePollScreenUITests.swift @@ -36,7 +36,7 @@ class CreatePollScreenUITests: XCTestCase { let option2TextField = app.textFields[A11yIdentifiers.createPollScreen.optionID(1)] option2TextField.tap() - option2TextField.typeText("No") + option2TextField.typeText("No\n") let createButton = app.buttons[A11yIdentifiers.createPollScreen.create] XCTAssertTrue(createButton.isEnabled) diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.createPoll-1.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.createPoll-1.png index 3fd0f4266..91c93e532 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.createPoll-1.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.createPoll-1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29a2bba04064c1ed0e839bf7fe29d51bf2c6488b0e4e0992deecff1c30aeb092 -size 157226 +oid sha256:6d5ff2fccc554a739052160b4e224f2845ac09d1bfb733c8f5adb78b860aa6a2 +size 86358 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.createPoll-1.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.createPoll-1.png index 68cba63d1..071776f5b 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.createPoll-1.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.createPoll-1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a934baa838db31b290f69fff6037d6764789615a25f34c94b8273abde4b8c83a -size 170170 +oid sha256:75e7080c7e2701f3c2225543f0862366af3726b289832f718a1f20687f718852 +size 98596 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.createPoll-1.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.createPoll-1.png index 3c9602c3f..eb299f39a 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.createPoll-1.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.createPoll-1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e8de87274b05142ffd494ef3e539e222241461521d9c12da6c60fe2bb8a894de -size 161620 +oid sha256:a3f917bd4c44eaefe4e1d81333ea0f957ab58ea30eb7d9bbfc74b9d341e64107 +size 90650 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.createPoll-1.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.createPoll-1.png index aab743579..52ad2a1a9 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.createPoll-1.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.createPoll-1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:75784ba1bcc96f0db74fa476073241c7e27dc809b35516a4a7fe35ee127b5748 -size 183973 +oid sha256:6f58f283d752f71111d6256fb4900811d4acd5c9986e49974079929eb8886e0b +size 113717