Introduce a TimelineItemThreadSummary object (#4032)

* Introduce a `TimelineItemThreadSummary` object to hold details about threads starting from that particular item

This patch introduces a thread summary object that will be available on main timeline messages that are the root for a given thread.
It currently provides the latest message content and sender for that thread but it will grow to provide info on the number of replies, unreads etc.

It also add a new UI component called `TimelineThreadSummaryView` that makes use of this data and is in turn used by the bubbled styler to render it in the timeline.

The rest of the PR is about refactoring on the `RoomTimelineItemFactory` so that replies and thread summaries use similar paths and builders.

* Add a feature flag for threads

* Address PR comments

* Converge on single implementation for message previews
This commit is contained in:
Stefan Ceriu
2025-04-16 16:19:29 +03:00
committed by GitHub
parent 8c9773e4de
commit 806d676299
34 changed files with 689 additions and 134 deletions
@@ -59,6 +59,7 @@ enum TimelineViewAction {
case displayReactionSummary(itemID: TimelineItemIdentifier, key: String)
case displayEmojiPicker(itemID: TimelineItemIdentifier)
case displayReadReceipts(itemID: TimelineItemIdentifier)
case displayThread(itemID: TimelineItemIdentifier)
case handlePasteOrDrop(provider: NSItemProvider)
case handlePollAction(TimelineViewPollAction)
@@ -102,6 +103,7 @@ struct TimelineViewState: BindableState {
var canCurrentUserKick = false
var canCurrentUserBan = false
var isViewSourceEnabled: Bool
var areThreadsEnabled: Bool
var hideTimelineMedia: Bool
// The `pinnedEventIDs` are used only to determine if an item is already pinned or not.
@@ -238,3 +240,56 @@ enum ScrollDirection: Equatable {
case top
case bottom
}
extension TimelineViewState {
/// 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.
func buildMessagePreview(formattedBody: AttributedString?, plainBody: String) -> 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 = 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: PillUtilities.atRoom)
}
if let roomAlias = attributes[.MatrixRoomAlias] as? String {
let roomName = roomNameForAliasResolver?(roomAlias)
attributedString.replaceCharacters(in: range, with: PillUtilities.roomPillDisplayText(roomName: roomName, rawRoomText: roomAlias))
}
if let roomID = attributes[.MatrixRoomID] as? String {
let roomName = roomNameForIDResolver?(roomID)
attributedString.replaceCharacters(in: range, with: PillUtilities.roomPillDisplayText(roomName: roomName, rawRoomText: roomID))
}
if let eventOnRoomID = attributes[.MatrixEventOnRoomID] as? EventOnRoomIDAttribute.Value {
let roomID = eventOnRoomID.roomID
let roomName = roomNameForIDResolver?(roomID)
attributedString.replaceCharacters(in: range, with: PillUtilities.eventPillDisplayText(roomName: roomName, rawRoomText: roomID))
}
if let eventOnRoomAlias = attributes[.MatrixEventOnRoomAlias] as? EventOnRoomAliasAttribute.Value {
let roomAlias = eventOnRoomAlias.alias
let roomName = roomNameForAliasResolver?(roomAlias)
attributedString.replaceCharacters(in: range, with: PillUtilities.eventPillDisplayText(roomName: roomName, rawRoomText: eventOnRoomAlias.alias))
}
}
return attributedString.string
}
}