From 83594b4d2f9775bb0ff322af75b4f524c4c94d00 Mon Sep 17 00:00:00 2001 From: Lukas <134181853+bo2themax@users.noreply.github.com> Date: Mon, 5 Jan 2026 14:03:53 +0100 Subject: [PATCH] Add "Translate" to TimelineItemMenuActions (#4846) * Add "Translate" to TimelineItemMenuActions * Update TimelineView.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> * Update strings using `download-strings` * Update ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> * Clear `textToBeTranslated` after translation was dismissed * `swift run tools download-strings --all-languages` --------- Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> # Conflicts: # ElementX/Resources/Localizations/et.lproj/Localizable.strings # ElementX/Resources/Localizations/fr.lproj/Localizable.strings # ElementX/Resources/Localizations/hr.lproj/Localizable.strings --- .../Screens/Timeline/TimelineInteractionHandler.swift | 4 ++++ ElementX/Sources/Screens/Timeline/TimelineModels.swift | 3 +++ ElementX/Sources/Screens/Timeline/TimelineViewModel.swift | 3 +++ .../Timeline/View/ItemMenu/TimelineItemMenuAction.swift | 8 ++++++++ .../View/ItemMenu/TimelineItemMenuActionProvider.swift | 1 + ElementX/Sources/Screens/Timeline/View/TimelineView.swift | 8 ++++++++ 6 files changed, 27 insertions(+) diff --git a/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift b/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift index 7e04ec696..6e6c70090 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift @@ -26,6 +26,7 @@ enum TimelineInteractionHandlerAction { case viewInRoomTimeline(eventID: String) case displayThread(itemID: TimelineItemIdentifier) + case showTranslation(text: String) } /// The interaction handler groups logic for dealing with various actions the user can take on a timeline's @@ -198,6 +199,9 @@ class TimelineInteractionHandler { break // Handled inline in the media preview screen with a ShareLink. case .save: break // Handled inline in the media preview screen. + case .translate: + guard let messageTimelineItem = timelineItem as? EventBasedMessageTimelineItemProtocol else { return } + actionsSubject.send(.showTranslation(text: messageTimelineItem.body)) } if action.switchToDefaultComposer { diff --git a/ElementX/Sources/Screens/Timeline/TimelineModels.swift b/ElementX/Sources/Screens/Timeline/TimelineModels.swift index 024cb5ced..4177d5cd4 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineModels.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineModels.swift @@ -160,6 +160,9 @@ struct TimelineViewStateBindings { var readReceiptsSummaryInfo: ReadReceiptSummaryInfo? var manageMemberViewModel: ManageRoomMemberSheetViewModel? + + var showTranslation = false + var textToBeTranslated: String? } struct TimelineItemActionMenuInfo: Equatable, Identifiable { diff --git a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift index 2a469fc89..be5b9356b 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift @@ -498,6 +498,9 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { Task { await self.viewInRoomTimeline(eventID: eventID) } case .displayThread(let itemID): actionsSubject.send(.displayThread(itemID: itemID)) + case .showTranslation(let text): + self.state.bindings.textToBeTranslated = text + self.state.bindings.showTranslation = true } } .store(in: &cancellables) diff --git a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift index 01e0f0210..9fd2a5abe 100644 --- a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift +++ b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift @@ -57,6 +57,7 @@ struct TimelineItemMenuReaction: Hashable { enum TimelineItemMenuAction: Identifiable, Hashable { case copy + case translate case copyCaption case edit case addCaption @@ -145,6 +146,13 @@ enum TimelineItemMenuAction: Identifiable, Hashable { switch self { case .copy: Label(L10n.actionCopyText, icon: \.copy) + case .translate: + Label { Text(L10n.actionTranslate) } icon: { + Image(systemSymbol: .translate) + .resizable() + .aspectRatio(contentMode: .fit) + .scaledFrame(size: 24) + } case .copyCaption: Label(L10n.actionCopyCaption, icon: \.copy) case .edit: diff --git a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift index 1569c4c28..61895b6fe 100644 --- a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift +++ b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift @@ -91,6 +91,7 @@ struct TimelineItemMenuActionProvider { if item.isCopyable { actions.append(.copy) + actions.append(.translate) } else if item.hasMediaCaption { actions.append(.copyCaption) } diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineView.swift index 261bbd0ea..aadeef6dc 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import Translation import WysiwygComposer struct TimelineView: View { @@ -55,6 +56,13 @@ struct TimelineView: View { ReadReceiptsSummaryView(orderedReadReceipts: $0.orderedReceipts) .environmentObject(timelineContext) } + .translationPresentation(isPresented: $timelineContext.showTranslation, text: timelineContext.textToBeTranslated ?? "") + .onChange(of: timelineContext.showTranslation) { oldValue, newValue in + if oldValue, !newValue { + // clear texts after translation was dismissed + timelineContext.textToBeTranslated = nil + } + } .onDrop(of: ["public.item", "public.file-url"], isTargeted: $dragOver) { providers -> Bool in let supportedProviders = providers.filter(\.isSupportedForPasteOrDrop)