diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 024d9807f..d1423b900 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -56,6 +56,7 @@ 05FF0CD80EDAB3A7C0D4700A /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */; }; 0638CBDE3098B1C3F23AFCFA /* MXLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 111B698739E3410E2CDB7144 /* MXLog.swift */; }; 065EAB39F3F3AB4F6BD2A362 /* AppLockSetupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19DD166C3625EE426203FA29 /* AppLockSetupTests.swift */; }; + 0668BECCF11E8EE6936CDF16 /* TimelineItemMenuActionProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C98C243A90369929D669873D /* TimelineItemMenuActionProviderTests.swift */; }; 066A1E9B94723EE9F3038044 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.swift */; }; 06B55882911B4BF5B14E9851 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227AC5D71A4CE43512062243 /* URL.swift */; }; 06D17F7813AA931FF18FD5D0 /* SDKListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5CD2993048222B64C45006 /* SDKListener.swift */; }; @@ -2924,6 +2925,7 @@ C90514BE9B8ACCBCF0AD2489 /* ComposerToolbarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbarViewModel.swift; sourceTree = ""; }; C95ADE8D9527523572532219 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = hu; path = hu.lproj/Localizable.stringsdict; sourceTree = ""; }; C97F8963B14EB0AF3940DDBF /* NotificationSettingsEditScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenRoomCell.swift; sourceTree = ""; }; + C98C243A90369929D669873D /* TimelineItemMenuActionProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMenuActionProviderTests.swift; sourceTree = ""; }; C99FDEEB71173C4C6FA2734C /* UserSessionFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionFlowCoordinator.swift; sourceTree = ""; }; C9AC2CC94FA06F728883B694 /* KnockRequestsListScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnockRequestsListScreenViewModelTests.swift; sourceTree = ""; }; C9E535B3388755B65C34CD10 /* UnsupportedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsupportedRoomTimelineView.swift; sourceTree = ""; }; @@ -5275,6 +5277,7 @@ 6DF438EAFC732D2D95D34BF6 /* StartChatViewModelTests.swift */, 2CEBCB9676FCD1D0F13188DD /* StringTests.swift */, 9AA3AF94A06D319BB37E52DA /* TimelineItemFactoryTests.swift */, + C98C243A90369929D669873D /* TimelineItemMenuActionProviderTests.swift */, ED0AD0C652385F69FA90FAF5 /* TimelineMediaPreviewDataSourceTests.swift */, 5C1F000589F2CEE6B03ECFAB /* TimelineMediaPreviewViewModelTests.swift */, F968FDB68F9D4CB9010FED70 /* TimelineStateTests.swift */, @@ -8372,6 +8375,7 @@ 1FEC0A4EC6E6DF693C16B32A /* StringTests.swift in Sources */, E75CE800B3E64D0F7F8E228D /* TemplateScreenViewModelTests.swift in Sources */, 0D4EB2ABAA5FE8CB10FDBCB8 /* TimelineItemFactoryTests.swift in Sources */, + 0668BECCF11E8EE6936CDF16 /* TimelineItemMenuActionProviderTests.swift in Sources */, C02DE5F62C81FB9E173C3D2F /* TimelineMediaPreviewDataSourceTests.swift in Sources */, F6BF52CB027393EE03CEC523 /* TimelineMediaPreviewViewModelTests.swift in Sources */, 75E55A763FE8BA64FB8513E4 /* TimelineStateTests.swift in Sources */, diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift index a1a702d3c..7dfd5d3f6 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift @@ -28,7 +28,7 @@ nonisolated extension EventBasedTimelineItemProtocol { } var isForwardable: Bool { - isRemoteMessage && !(self is PollRoomTimelineItem) + isRemoteMessage && !(self is PollRoomTimelineItem) && !(self is LiveLocationRoomTimelineItem) } var isRemoteMessage: Bool { diff --git a/UnitTests/Sources/TimelineItemMenuActionProviderTests.swift b/UnitTests/Sources/TimelineItemMenuActionProviderTests.swift new file mode 100644 index 000000000..ec7f1c70e --- /dev/null +++ b/UnitTests/Sources/TimelineItemMenuActionProviderTests.swift @@ -0,0 +1,88 @@ +// +// Copyright 2026 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +@testable import ElementX +import Testing + +struct TimelineItemMenuActionProviderTests { + @Test + func liveLocationShareIsNotForwardable() throws { + let item = makeLiveLocationItem(isLive: true) + let actions = try #require(makeActions(for: item)) + + let hasForward = actions.actions.contains(where: \.isForward) + #expect(!hasForward) + } + + @Test + func endedLiveLocationShareIsNotForwardable() throws { + let item = makeLiveLocationItem(isLive: false) + let actions = try #require(makeActions(for: item)) + + let hasForward = actions.actions.contains(where: \.isForward) + #expect(!hasForward) + } + + @Test + func pollIsNotForwardable() throws { + let item = PollRoomTimelineItem.mock(poll: .emptyDisclosed) + let actions = try #require(makeActions(for: item)) + + let hasForward = actions.actions.contains(where: \.isForward) + #expect(!hasForward) + } + + @Test + func textMessageIsForwardable() throws { + let item = TextRoomTimelineItem(id: .randomEvent, + timestamp: .mock, + isOutgoing: false, + isEditable: false, + canBeRepliedTo: true, + sender: .init(id: "@alice:matrix.org"), + content: .init(body: "Hello")) + let actions = try #require(makeActions(for: item)) + + let hasForward = actions.actions.contains(where: \.isForward) + #expect(hasForward) + } + + // MARK: - Helpers + + private func makeLiveLocationItem(isLive: Bool) -> LiveLocationRoomTimelineItem { + .init(id: .randomEvent, + timestamp: .mock, + isOutgoing: false, + isEditable: false, + canBeRepliedTo: true, + sender: .init(id: "@alice:matrix.org"), + content: .init(isLive: isLive, timeoutDate: .mock, lastGeoURI: nil)) + } + + private func makeActions(for item: RoomTimelineItemProtocol) -> TimelineItemMenuActions? { + TimelineItemMenuActionProvider(timelineItem: item, + canCurrentUserSendMessage: true, + canCurrentUserRedactSelf: true, + canCurrentUserRedactOthers: false, + canCurrentUserPin: true, + pinnedEventIDs: [], + isViewSourceEnabled: true, + areThreadsEnabled: true, + timelineKind: .live, + emojiProvider: EmojiProvider(appSettings: .volatile())) + .makeActions() + } +} + +private extension TimelineItemMenuAction { + var isForward: Bool { + if case .forward = self { + return true + } + return false + } +}