diff --git a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift index 1baddfff9..095afb438 100644 --- a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift +++ b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift @@ -122,6 +122,7 @@ struct PinnedEventsTimelineScreen_Previews: PreviewProvider, TestablePreview { timelineControllerFactory: TimelineControllerFactoryMock(.init())) if isSelecting { + timelineViewModel.state.messageSelection.isActive = true timelineViewModel.state.messageSelection.selectedEventIDs = Set(eventIDs.prefix(2)) } diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index 731521f4a..8b4d90953 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -413,6 +413,7 @@ struct RoomScreen_Previews: PreviewProvider, TestablePreview { if isSelecting { let eventIDs = timelineController.timelineItems.compactMap { ($0 as? EventBasedTimelineItemProtocol)?.id.eventID } + timelineViewModel.state.messageSelection.isActive = true timelineViewModel.state.messageSelection.selectedEventIDs = Set(eventIDs.prefix(2)) } diff --git a/ElementX/Sources/Screens/Timeline/TimelineModels.swift b/ElementX/Sources/Screens/Timeline/TimelineModels.swift index 240bfc7c1..e8d6c2119 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineModels.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineModels.swift @@ -205,17 +205,15 @@ struct TimelineViewStateBindings { var textToBeTranslated: String? } -/// The state of the multi-selection of messages, active as soon as an item is selected. +/// The state of the multi-selection of messages. struct TimelineMessageSelectionState: Equatable { static let limit = 10 var isEnabled = false + /// Stays on when every message is unselected, it only ends when closed or once the messages are forwarded. + var isActive = false var selectedEventIDs: Set = [] - var isActive: Bool { - !selectedEventIDs.isEmpty - } - var count: Int { selectedEventIDs.count } @@ -228,6 +226,11 @@ struct TimelineMessageSelectionState: Equatable { guard let eventID else { return false } return selectedEventIDs.contains(eventID) } + + mutating func end() { + isActive = false + selectedEventIDs.removeAll() + } } struct TimelineItemActionMenuInfo: Equatable, Identifiable { diff --git a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift index 01c61790d..30f837a5c 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift @@ -1200,7 +1200,7 @@ extension TimelineViewModel { .sink { [weak self] isEnabled in self?.state.messageSelection.isEnabled = isEnabled if !isEnabled { - self?.state.messageSelection.selectedEventIDs.removeAll() + self?.state.messageSelection.end() } } .store(in: &cancellables) @@ -1218,6 +1218,7 @@ extension TimelineViewModel { return } + state.messageSelection.isActive = true state.messageSelection.selectedEventIDs.insert(eventID) } @@ -1239,7 +1240,7 @@ extension TimelineViewModel { guard state.messageSelection.isActive else { return } if isSwitchingTimelines { - state.messageSelection.selectedEventIDs.removeAll() + state.messageSelection.end() return } @@ -1249,7 +1250,7 @@ extension TimelineViewModel { /// Ends the selection, either from the close button or once the selected messages have been forwarded. func clearMessageSelection() { - state.messageSelection.selectedEventIDs.removeAll() + state.messageSelection.end() } /// Forwards the selection in timeline order, which is unrelated to the order the messages were selected in. diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift index 7b1347c3a..5604c3b44 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift @@ -476,6 +476,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview emojiProvider: EmojiProvider(userSettings: userSettings), linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) + viewModel.state.messageSelection.isActive = true viewModel.state.messageSelection.selectedEventIDs = ["selected"] return viewModel }() diff --git a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineMessageSelectionActionBar.swift b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineMessageSelectionActionBar.swift index c531f2ff8..98bad2b29 100644 --- a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineMessageSelectionActionBar.swift +++ b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineMessageSelectionActionBar.swift @@ -19,6 +19,7 @@ struct TimelineMessageSelectionActionBar: View { Label(L10n.actionForwardMessages(context.viewState.messageSelection.count), icon: \.forward) } .buttonStyle(.compound(.primary)) + .disabled(context.viewState.messageSelection.selectedEventIDs.isEmpty) .padding(16) .background(Color.compound.bgCanvasDefault.ignoresSafeArea()) } @@ -27,14 +28,23 @@ struct TimelineMessageSelectionActionBar: View { // MARK: - Previews struct TimelineMessageSelectionActionBar_Previews: PreviewProvider, TestablePreview { - static let viewModel: TimelineViewModel = { - let viewModel = TimelineViewModel.mock() // A fresh instance so the shared mock isn't left selecting. - viewModel.state.messageSelection.selectedEventIDs = ["$1", "$2", "$3"] - return viewModel - }() + static let viewModel = makeViewModel(selectedEventIDs: ["$1", "$2", "$3"]) + static let emptyViewModel = makeViewModel(selectedEventIDs: []) static var previews: some View { TimelineMessageSelectionActionBar(context: viewModel.context) .previewLayout(.sizeThatFits) + .previewDisplayName("Selected") + + TimelineMessageSelectionActionBar(context: emptyViewModel.context) + .previewLayout(.sizeThatFits) + .previewDisplayName("Nothing selected") + } + + static func makeViewModel(selectedEventIDs: Set) -> TimelineViewModel { + let viewModel = TimelineViewModel.mock() // A fresh instance so the shared mock isn't left selecting. + viewModel.state.messageSelection.isActive = true + viewModel.state.messageSelection.selectedEventIDs = selectedEventIDs + return viewModel } } diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineMessageSelectionActionBar.Nothing-selected-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineMessageSelectionActionBar.Nothing-selected-iPad-en-GB.png new file mode 100644 index 000000000..d6ea898fd --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineMessageSelectionActionBar.Nothing-selected-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:240161bc2235106fd4e74d0c25b3d82fa5bf2380425441a2db101693429afbfe +size 16087 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineMessageSelectionActionBar.Nothing-selected-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineMessageSelectionActionBar.Nothing-selected-iPad-pseudo.png new file mode 100644 index 000000000..097e6ffc7 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineMessageSelectionActionBar.Nothing-selected-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22abae64ed63c2ac1f7d317524a75fe9114f62b299bbf6f17973e6a593c881b5 +size 16241 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineMessageSelectionActionBar.Nothing-selected-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineMessageSelectionActionBar.Nothing-selected-iPhone-en-GB.png new file mode 100644 index 000000000..5f81524c7 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineMessageSelectionActionBar.Nothing-selected-iPhone-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:338fbbe237edd16a6720c7f66544fc79db607e114909a491d2935cc397eac40c +size 10566 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineMessageSelectionActionBar.Nothing-selected-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineMessageSelectionActionBar.Nothing-selected-iPhone-pseudo.png new file mode 100644 index 000000000..ccd6635b2 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineMessageSelectionActionBar.Nothing-selected-iPhone-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d2e8a5312d7b2ecce636347b369d9802d93242583f745887f70baefe6a5e5e1 +size 10642 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineMessageSelectionActionBar.iPad-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineMessageSelectionActionBar.Selected-iPad-en-GB.png similarity index 100% rename from PreviewTests/Sources/__Snapshots__/PreviewTests/timelineMessageSelectionActionBar.iPad-en-GB-0.png rename to PreviewTests/Sources/__Snapshots__/PreviewTests/timelineMessageSelectionActionBar.Selected-iPad-en-GB.png diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineMessageSelectionActionBar.iPad-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineMessageSelectionActionBar.Selected-iPad-pseudo.png similarity index 100% rename from PreviewTests/Sources/__Snapshots__/PreviewTests/timelineMessageSelectionActionBar.iPad-pseudo-0.png rename to PreviewTests/Sources/__Snapshots__/PreviewTests/timelineMessageSelectionActionBar.Selected-iPad-pseudo.png diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineMessageSelectionActionBar.iPhone-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineMessageSelectionActionBar.Selected-iPhone-en-GB.png similarity index 100% rename from PreviewTests/Sources/__Snapshots__/PreviewTests/timelineMessageSelectionActionBar.iPhone-en-GB-0.png rename to PreviewTests/Sources/__Snapshots__/PreviewTests/timelineMessageSelectionActionBar.Selected-iPhone-en-GB.png diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineMessageSelectionActionBar.iPhone-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineMessageSelectionActionBar.Selected-iPhone-pseudo.png similarity index 100% rename from PreviewTests/Sources/__Snapshots__/PreviewTests/timelineMessageSelectionActionBar.iPhone-pseudo-0.png rename to PreviewTests/Sources/__Snapshots__/PreviewTests/timelineMessageSelectionActionBar.Selected-iPhone-pseudo.png diff --git a/UnitTests/Sources/TimelineViewModelTests.swift b/UnitTests/Sources/TimelineViewModelTests.swift index d5977b8f2..b8a0bd84b 100644 --- a/UnitTests/Sources/TimelineViewModelTests.swift +++ b/UnitTests/Sources/TimelineViewModelTests.swift @@ -781,9 +781,14 @@ final class TimelineViewModelTests { viewModel.process(viewAction: .toggleMessageSelection(itemID: items[0].id)) #expect(viewModel.state.messageSelection.selectedEventIDs == ["$2"]) - // Deselecting the last item ends the selection. + // Deselecting the last item keeps the selection going, with nothing selected. viewModel.process(viewAction: .toggleMessageSelection(itemID: items[1].id)) - #expect(!viewModel.state.messageSelection.isActive) + #expect(viewModel.state.messageSelection.isActive) + #expect(viewModel.state.messageSelection.selectedEventIDs.isEmpty) + + // Selecting again still works. + viewModel.process(viewAction: .toggleMessageSelection(itemID: items[0].id)) + #expect(viewModel.state.messageSelection.selectedEventIDs == ["$1"]) } @Test