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
This commit is contained in:
Lukas
2026-01-05 14:03:53 +01:00
committed by Doug
parent 784bc04237
commit ec97996a7c
6 changed files with 27 additions and 0 deletions
@@ -26,6 +26,7 @@ enum TimelineInteractionHandlerAction {
case viewInRoomTimeline(eventID: String) case viewInRoomTimeline(eventID: String)
case displayThread(itemID: TimelineItemIdentifier) 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 /// 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. break // Handled inline in the media preview screen with a ShareLink.
case .save: case .save:
break // Handled inline in the media preview screen. 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 { if action.switchToDefaultComposer {
@@ -160,6 +160,9 @@ struct TimelineViewStateBindings {
var readReceiptsSummaryInfo: ReadReceiptSummaryInfo? var readReceiptsSummaryInfo: ReadReceiptSummaryInfo?
var manageMemberViewModel: ManageRoomMemberSheetViewModel? var manageMemberViewModel: ManageRoomMemberSheetViewModel?
var showTranslation = false
var textToBeTranslated: String?
} }
struct TimelineItemActionMenuInfo: Equatable, Identifiable { struct TimelineItemActionMenuInfo: Equatable, Identifiable {
@@ -498,6 +498,9 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
Task { await self.viewInRoomTimeline(eventID: eventID) } Task { await self.viewInRoomTimeline(eventID: eventID) }
case .displayThread(let itemID): case .displayThread(let itemID):
actionsSubject.send(.displayThread(itemID: itemID)) actionsSubject.send(.displayThread(itemID: itemID))
case .showTranslation(let text):
self.state.bindings.textToBeTranslated = text
self.state.bindings.showTranslation = true
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -57,6 +57,7 @@ struct TimelineItemMenuReaction: Hashable {
enum TimelineItemMenuAction: Identifiable, Hashable { enum TimelineItemMenuAction: Identifiable, Hashable {
case copy case copy
case translate
case copyCaption case copyCaption
case edit case edit
case addCaption case addCaption
@@ -145,6 +146,13 @@ enum TimelineItemMenuAction: Identifiable, Hashable {
switch self { switch self {
case .copy: case .copy:
Label(L10n.actionCopyText, icon: \.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: case .copyCaption:
Label(L10n.actionCopyCaption, icon: \.copy) Label(L10n.actionCopyCaption, icon: \.copy)
case .edit: case .edit:
@@ -91,6 +91,7 @@ struct TimelineItemMenuActionProvider {
if item.isCopyable { if item.isCopyable {
actions.append(.copy) actions.append(.copy)
actions.append(.translate)
} else if item.hasMediaCaption { } else if item.hasMediaCaption {
actions.append(.copyCaption) actions.append(.copyCaption)
} }
@@ -7,6 +7,7 @@
// //
import SwiftUI import SwiftUI
import Translation
import WysiwygComposer import WysiwygComposer
struct TimelineView: View { struct TimelineView: View {
@@ -55,6 +56,13 @@ struct TimelineView: View {
ReadReceiptsSummaryView(orderedReadReceipts: $0.orderedReceipts) ReadReceiptsSummaryView(orderedReadReceipts: $0.orderedReceipts)
.environmentObject(timelineContext) .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 .onDrop(of: ["public.item", "public.file-url"], isTargeted: $dragOver) { providers -> Bool in
let supportedProviders = providers.filter(\.isSupportedForPasteOrDrop) let supportedProviders = providers.filter(\.isSupportedForPasteOrDrop)