diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenInteractionHandler.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenInteractionHandler.swift index e9dd8cbc1..cb2cf4050 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenInteractionHandler.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenInteractionHandler.swift @@ -257,6 +257,9 @@ class RoomScreenInteractionHandler { actionsSubject.send(.displayReportContent(itemID: itemID, senderID: eventTimelineItem.sender.id)) case .react: showEmojiPicker(for: itemID) + case .toggleReaction(let key): + guard let eventID = itemID.eventID else { return } + Task { await roomProxy.timeline.toggleReaction(key, to: eventID) } case .endPoll(let pollStartID): endPoll(pollStartID: pollStartID) } diff --git a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMacContextMenu.swift b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMacContextMenu.swift index d6325b544..d0d6b0171 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMacContextMenu.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMacContextMenu.swift @@ -14,6 +14,8 @@ // limitations under the License. // +import Compound +import SFSafeSymbols import SwiftUI /// The contents of the context menu shown when right clicking an item in the timeline on a Mac @@ -27,8 +29,24 @@ struct TimelineItemMacContextMenu: View { if let menuActions = actionProvider?(item.id) { Section { if item.isReactable { - Button { send(.react) } label: { - TimelineItemMenuAction.react.label + if #available(iOS 17.0, *) { + let reactions = (item as? EventBasedTimelineItemProtocol)?.properties.reactions ?? [] + ControlGroup { + ForEach(menuActions.reactions, id: \.key) { + ReactionToggle(reaction: $0, reactions: reactions) { + send(.toggleReaction(key: $0)) + } + } + + Button { send(.react) } label: { + CompoundIcon(\.reactionAdd) + } + } + .controlGroupStyle(.palette) + } else { + Button { send(.react) } label: { + TimelineItemMenuAction.react.label + } } } @@ -54,3 +72,21 @@ struct TimelineItemMacContextMenu: View { } } } + +/// A button that acts as a toggle for reacting to a message. +private struct ReactionToggle: View { + let reaction: TimelineItemMenuReaction + let reactions: [AggregatedReaction] + let action: (String) -> Void + + var isOn: Bool { + reactions.contains { $0.key == reaction.key && $0.isHighlighted } + } + + var body: some View { + Button { action(reaction.key) } label: { + Image(systemSymbol: reaction.symbol) + .symbolVariant(isOn ? .fill : .none) + } + } +} diff --git a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMenu.swift b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMenu.swift index 9266ce024..7c20573b3 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMenu.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMenu.swift @@ -15,9 +15,11 @@ // import Compound +import SFSafeSymbols import SwiftUI struct TimelineItemMenuActions { + let reactions: [TimelineItemMenuReaction] let actions: [TimelineItemMenuAction] let debugActions: [TimelineItemMenuAction] @@ -28,6 +30,13 @@ struct TimelineItemMenuActions { self.actions = actions self.debugActions = debugActions + reactions = [ + .init(key: "👍️", symbol: .handThumbsup), + .init(key: "👎️", symbol: .handThumbsdown), + .init(key: "🔥", symbol: .flame), + .init(key: "❤️", symbol: .heart), + .init(key: "👏", symbol: .handsClap) + ] } var canReply: Bool { @@ -41,6 +50,11 @@ struct TimelineItemMenuActions { } } +struct TimelineItemMenuReaction { + let key: String + let symbol: SFSymbol +} + enum TimelineItemMenuAction: Identifiable, Hashable { case copy case edit @@ -52,6 +66,7 @@ enum TimelineItemMenuAction: Identifiable, Hashable { case retryDecryption(sessionID: String) case report case react + case toggleReaction(key: String) case endPoll(pollStartID: String) var id: Self { self } @@ -120,6 +135,9 @@ enum TimelineItemMenuAction: Identifiable, Hashable { Label(L10n.actionReportContent, icon: \.chatProblem) case .react: Label(L10n.actionReact, icon: \.reactionAdd) + case .toggleReaction: + // Unused label - manually created in TimelineItemMacContextMenu. + Label(L10n.actionReact, icon: \.reactionAdd) case .endPoll: Label(L10n.actionEndPoll, icon: \.pollsEnd) } @@ -216,11 +234,9 @@ struct TimelineItemMenu: View { private var reactionsSection: some View { ScrollView(.horizontal) { HStack(alignment: .center, spacing: 8) { - reactionButton(for: "👍️") - reactionButton(for: "👎️") - reactionButton(for: "🔥") - reactionButton(for: "❤️") - reactionButton(for: "👏") + ForEach(actions.reactions, id: \.key) { + reactionButton(for: $0.key) + } Button { dismiss()