From 89fc190e9d3654d25f44d45ef6f7894280867b12 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 16 Apr 2024 09:16:06 +0300 Subject: [PATCH] element-hq/element-x-ios/issues/2636 - Expose paths for focusing replied-to timeline items by tapping on an in-reply-to message bubble --- .../Screens/RoomScreen/RoomScreenModels.swift | 4 ++ .../RoomScreen/RoomScreenViewModel.swift | 3 + .../View/Replies/TimelineReplyView.swift | 62 ++++++++--------- .../Style/TimelineItemBubbledStylerView.swift | 20 ++++-- .../Style/TimelineItemPlainStylerView.swift | 68 +++++++++++++++---- 5 files changed, 102 insertions(+), 55 deletions(-) diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 6d489f50e7..92cef30440 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -76,8 +76,10 @@ enum RoomScreenViewAudioAction { enum RoomScreenViewAction { case displayRoomDetails + case itemAppeared(itemID: TimelineItemIdentifier) case itemDisappeared(itemID: TimelineItemIdentifier) + case itemTapped(itemID: TimelineItemIdentifier) case toggleReaction(key: String, itemID: TimelineItemIdentifier) case sendReadReceiptIfNeeded(TimelineItemIdentifier) @@ -104,6 +106,8 @@ enum RoomScreenViewAction { case audio(RoomScreenViewAudioAction) case presentCall + + case focusOnItem(itemID: TimelineItemIdentifier) } enum RoomScreenComposerAction { diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 82466e2d4d..e99e80df37 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -172,6 +172,9 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol actionsSubject.send(.displayCallScreen) case .showReadReceipts(itemID: let itemID): showReadReceipts(for: itemID) + case .focusOnItem(itemID: let itemID): + // TODO: .. something + break } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Replies/TimelineReplyView.swift b/ElementX/Sources/Screens/RoomScreen/View/Replies/TimelineReplyView.swift index 44c66515f1..629f5ab0e9 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Replies/TimelineReplyView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Replies/TimelineReplyView.swift @@ -131,38 +131,6 @@ struct TimelineReplyView: View { var icon: Icon? - var isTextOnly: Bool { - icon == nil - } - - /// The string shown as the message preview. - /// - /// This converts the formatted body to a plain string to remove formatting - /// and render with a consistent font size. This conversion is done to avoid - /// showing markdown characters in the preview for messages with formatting. - var messagePreview: String { - guard let formattedBody, - let attributedString = try? NSMutableAttributedString(formattedBody, including: \.elementX) else { - return plainBody - } - - let range = NSRange(location: 0, length: attributedString.length) - attributedString.enumerateAttributes(in: range) { attributes, range, _ in - if let userID = attributes[.MatrixUserID] as? String { - if let displayName = context.viewState.members[userID]?.displayName { - attributedString.replaceCharacters(in: range, with: "@\(displayName)") - } else { - attributedString.replaceCharacters(in: range, with: userID) - } - } - - if attributes[.MatrixAllUsersMention] as? Bool == true { - attributedString.replaceCharacters(in: range, with: PillConstants.atRoom) - } - } - return attributedString.string - } - var body: some View { HStack(spacing: 8) { iconView @@ -183,7 +151,7 @@ struct TimelineReplyView: View { .tint(.compound.textLinkExternal) .lineLimit(2) } - .padding(.leading, isTextOnly ? 8 : 0) + .padding(.leading, icon == nil ? 8 : 0) .padding(.trailing, 8) } } @@ -216,6 +184,34 @@ struct TimelineReplyView: View { } } } + + /// The string shown as the message preview. + /// + /// This converts the formatted body to a plain string to remove formatting + /// and render with a consistent font size. This conversion is done to avoid + /// showing markdown characters in the preview for messages with formatting. + private var messagePreview: String { + guard let formattedBody, + let attributedString = try? NSMutableAttributedString(formattedBody, including: \.elementX) else { + return plainBody + } + + let range = NSRange(location: 0, length: attributedString.length) + attributedString.enumerateAttributes(in: range) { attributes, range, _ in + if let userID = attributes[.MatrixUserID] as? String { + if let displayName = context.viewState.members[userID]?.displayName { + attributedString.replaceCharacters(in: range, with: "@\(displayName)") + } else { + attributedString.replaceCharacters(in: range, with: userID) + } + } + + if attributes[.MatrixAllUsersMention] as? Bool == true { + attributedString.replaceCharacters(in: range, with: PillConstants.atRoom) + } + } + return attributedString.string + } } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift index 7c6ae76f46..0d020a72bb 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift @@ -220,16 +220,22 @@ struct TimelineItemBubbledStylerView: View { .padding(.leading, 4) .layoutPriority(TimelineBubbleLayout.Priority.regularText) } + if let replyDetails = messageTimelineItem.replyDetails { // The rendered reply bubble with a greedy width. The custom layout prevents // the infinite width from increasing the overall width of the view. - TimelineReplyView(placement: .timeline, timelineItemReplyDetails: replyDetails) - .fixedSize(horizontal: false, vertical: true) - .padding(4.0) - .frame(maxWidth: .infinity, alignment: .leading) - .background(Color.compound.bgCanvasDefault) - .cornerRadius(8) - .layoutPriority(TimelineBubbleLayout.Priority.visibleQuote) + + Button { + context.send(viewAction: .focusOnItem(itemID: timelineItem.id)) + } label: { + TimelineReplyView(placement: .timeline, timelineItemReplyDetails: replyDetails) + .fixedSize(horizontal: false, vertical: true) + .padding(4.0) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.compound.bgCanvasDefault) + .cornerRadius(8) + .layoutPriority(TimelineBubbleLayout.Priority.visibleQuote) + } // Add a fixed width reply bubble that is used for layout calculations but won't be rendered. TimelineReplyView(placement: .timeline, timelineItemReplyDetails: replyDetails) diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift index b37f8bcafc..7d71328e76 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift @@ -52,7 +52,12 @@ struct TimelineItemPlainStylerView: View { Rectangle() .foregroundColor(.compound.iconTertiary) .frame(width: 4.0) - TimelineReplyView(placement: .timeline, timelineItemReplyDetails: replyDetails) + + Button { + context.send(viewAction: .focusOnItem(itemID: timelineItem.id)) + } label: { + TimelineReplyView(placement: .timeline, timelineItemReplyDetails: replyDetails) + } } } } @@ -142,6 +147,28 @@ struct TimelineItemPlainStylerView: View { struct TimelineItemPlainStylerView_Previews: PreviewProvider, TestablePreview { static let viewModel = RoomScreenViewModel.mock + static var previews: some View { + VStack(alignment: .leading, spacing: 0) { + ForEach(1..