Skip to content

Commit

Permalink
/issues/2636 - Expose paths for focusing replied-to timeline items by…
Browse files Browse the repository at this point in the history
… tapping on an in-reply-to message bubble
  • Loading branch information
stefanceriu committed Apr 16, 2024
1 parent 43c1327 commit 89fc190
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 55 deletions.
4 changes: 4 additions & 0 deletions ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -104,6 +106,8 @@ enum RoomScreenViewAction {
case audio(RoomScreenViewAudioAction)

case presentCall

case focusOnItem(itemID: TimelineItemIdentifier)
}

enum RoomScreenComposerAction {
Expand Down
3 changes: 3 additions & 0 deletions ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
}
Expand Down Expand Up @@ -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
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,16 +220,22 @@ struct TimelineItemBubbledStylerView<Content: View>: 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,12 @@ struct TimelineItemPlainStylerView<Content: View>: 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)
}
}
}
}
Expand Down Expand Up @@ -142,6 +147,28 @@ struct TimelineItemPlainStylerView<Content: View>: View {
struct TimelineItemPlainStylerView_Previews: PreviewProvider, TestablePreview {
static let viewModel = RoomScreenViewModel.mock

static var previews: some View {
VStack(alignment: .leading, spacing: 0) {
ForEach(1..<MockRoomTimelineController().timelineItems.count, id: \.self) { index in
let item = MockRoomTimelineController().timelineItems[index]
RoomTimelineItemView(viewState: .init(item: item, groupStyle: .single))
.padding(TimelineStyle.plain.rowInsets) // Insets added in the table view cells
}
}
.environment(\.timelineStyle, .plain)
.environmentObject(viewModel.context)
.previewLayout(.sizeThatFits)

threads
.padding()
.environment(\.timelineStyle, .plain)
.previewDisplayName("Threads")

replies
.environment(\.timelineStyle, .plain)
.previewDisplayName("Replies")
}

// These akwats include a reply
static var threads: some View {
ScrollView {
Expand Down Expand Up @@ -237,21 +264,32 @@ struct TimelineItemPlainStylerView_Previews: PreviewProvider, TestablePreview {
.environmentObject(viewModel.context)
}

static var previews: some View {
VStack(alignment: .leading, spacing: 0) {
ForEach(1..<MockRoomTimelineController().timelineItems.count, id: \.self) { index in
let item = MockRoomTimelineController().timelineItems[index]
RoomTimelineItemView(viewState: .init(item: item, groupStyle: .single))
.padding(TimelineStyle.plain.rowInsets) // Insets added in the table view cells
}
static var replies: some View {
ScrollView {
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(timelineID: ""),
timestamp: "10:42",
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "whoever"),
content: .init(body: "A long message that should be on multiple lines."),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
eventContent: .message(.text(.init(body: "Short"))))),
groupStyle: .single))

RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: .init(timelineID: ""),
timestamp: "10:42",
isOutgoing: true,
isEditable: false,
canBeRepliedTo: true,
isThreaded: false,
sender: .init(id: "whoever"),
content: .init(body: "Short message"),
replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
eventContent: .message(.text(.init(body: "A long message that should be on more than 2 lines and so will be clipped by the layout."))))),
groupStyle: .single))
}
.environment(\.timelineStyle, .plain)
.environmentObject(viewModel.context)
.previewLayout(.sizeThatFits)

threads
.padding()
.environment(\.timelineStyle, .plain)
.previewDisplayName("Threads")
}
}

0 comments on commit 89fc190

Please sign in to comment.