Render the number of thread replies on the thread summary timeline view.

This commit is contained in:
Stefan Ceriu
2025-06-12 15:26:57 +03:00
committed by Stefan Ceriu
parent 264a68d3e2
commit 9309b543b8
19 changed files with 114 additions and 63 deletions

View File

@@ -8736,7 +8736,7 @@
repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift";
requirement = {
kind = exactVersion;
version = 25.06.12;
version = "25.06.12-2";
};
};
701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = {

View File

@@ -158,8 +158,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/element-hq/matrix-rust-components-swift",
"state" : {
"revision" : "6450696917e54b4ab62cad9275d673e43d0e865c",
"version" : "25.6.12"
"revision" : "0f239a906115a792bdca0741ca20fc59a64b8e39",
"version" : "25.6.12-2"
}
},
{

View File

@@ -222,6 +222,7 @@
"common_reason" = "Reason";
"common_recovery_key" = "Recovery key";
"common_refreshing" = "Refreshing…";
"common_replies" = "%1$d replies";
"common_replying_to" = "Replying to %1$@";
"common_report_a_bug" = "Report a bug";
"common_report_a_problem" = "Report a problem";
@@ -332,6 +333,7 @@
"error_no_compatible_app_found" = "No compatible app was found to handle this action.";
"error_some_messages_have_not_been_sent" = "Some messages have not been sent";
"error_unknown" = "Sorry, an error occurred";
"event_shield_mismatched_sender" = "The sender of the event does not match the owner of the device that sent it.";
"event_shield_reason_authenticity_not_guaranteed" = "The authenticity of this encrypted message can't be guaranteed on this device.";
"event_shield_reason_previously_verified" = "Encrypted by a previously-verified user.";
"event_shield_reason_sent_in_clear" = "Not encrypted.";

View File

@@ -512,6 +512,10 @@ internal enum L10n {
internal static var commonRecoveryKey: String { return L10n.tr("Localizable", "common_recovery_key") }
/// Refreshing
internal static var commonRefreshing: String { return L10n.tr("Localizable", "common_refreshing") }
/// %1$d replies
internal static func commonReplies(_ p1: Int) -> String {
return L10n.tr("Localizable", "common_replies", p1)
}
/// Replying to %1$@
internal static func commonReplyingTo(_ p1: Any) -> String {
return L10n.tr("Localizable", "common_replying_to", String(describing: p1))
@@ -764,6 +768,8 @@ internal enum L10n {
internal static var errorSomeMessagesHaveNotBeenSent: String { return L10n.tr("Localizable", "error_some_messages_have_not_been_sent") }
/// Sorry, an error occurred
internal static var errorUnknown: String { return L10n.tr("Localizable", "error_unknown") }
/// The sender of the event does not match the owner of the device that sent it.
internal static var eventShieldMismatchedSender: String { return L10n.tr("Localizable", "event_shield_mismatched_sender") }
/// The authenticity of this encrypted message can't be guaranteed on this device.
internal static var eventShieldReasonAuthenticityNotGuaranteed: String { return L10n.tr("Localizable", "event_shield_reason_authenticity_not_guaranteed") }
/// Encrypted by a previously-verified user.

View File

@@ -419,7 +419,8 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
ScrollView {
let threadSummary = TimelineItemThreadSummary.loaded(senderID: "@alice:matrix.org",
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
latestEventContent: .message(.text(.init(body: "This is a very long, multi-lined, threaded message"))))
latestEventContent: .message(.text(.init(body: "This is a very long, multi-lined, threaded message"))),
numberOfReplies: 42)
MockTimelineContent(threadSummary: threadSummary)
}

View File

@@ -23,7 +23,7 @@ struct TimelineThreadSummaryView: View {
@ViewBuilder
private var content: some View {
switch threadSummary {
case .loaded(let senderID, let sender, let latestEventContent):
case .loaded(let senderID, let sender, let latestEventContent, let numberOfReplies):
switch latestEventContent {
case .message(let content):
switch content {
@@ -31,58 +31,69 @@ struct TimelineThreadSummaryView: View {
ThreadView(senderID: senderID,
sender: sender,
plainBody: content.caption ?? content.filename,
formattedBody: content.formattedCaption)
formattedBody: content.formattedCaption,
numberOfReplies: numberOfReplies)
case .emote(let content):
ThreadView(senderID: senderID,
sender: sender,
plainBody: content.body,
formattedBody: content.formattedBody)
formattedBody: content.formattedBody,
numberOfReplies: numberOfReplies)
case .file(let content):
ThreadView(senderID: senderID,
sender: sender,
plainBody: content.caption ?? content.filename,
formattedBody: content.formattedCaption)
formattedBody: content.formattedCaption,
numberOfReplies: numberOfReplies)
case .image(let content):
ThreadView(senderID: senderID,
sender: sender,
plainBody: content.caption ?? content.filename,
formattedBody: content.formattedCaption)
formattedBody: content.formattedCaption,
numberOfReplies: numberOfReplies)
case .notice(let content):
ThreadView(senderID: senderID,
sender: sender,
plainBody: content.body,
formattedBody: content.formattedBody)
formattedBody: content.formattedBody,
numberOfReplies: numberOfReplies)
case .text(let content):
ThreadView(senderID: senderID,
sender: sender,
plainBody: content.body,
formattedBody: content.formattedBody)
formattedBody: content.formattedBody,
numberOfReplies: numberOfReplies)
case .video(let content):
ThreadView(senderID: senderID,
sender: sender,
plainBody: content.caption ?? content.filename,
formattedBody: content.formattedCaption)
formattedBody: content.formattedCaption,
numberOfReplies: numberOfReplies)
case .voice:
ThreadView(senderID: senderID,
sender: sender,
plainBody: L10n.commonVoiceMessage,
formattedBody: nil)
formattedBody: nil,
numberOfReplies: numberOfReplies)
case .location:
ThreadView(senderID: senderID,
sender: sender,
plainBody: L10n.commonSharedLocation,
formattedBody: nil)
formattedBody: nil,
numberOfReplies: numberOfReplies)
}
case .poll(let question):
ThreadView(senderID: senderID,
sender: sender,
plainBody: question,
formattedBody: nil)
formattedBody: nil,
numberOfReplies: numberOfReplies)
case .redacted:
ThreadView(senderID: senderID,
sender: sender,
plainBody: L10n.commonMessageRemoved,
formattedBody: nil)
formattedBody: nil,
numberOfReplies: numberOfReplies)
}
default:
LoadingThreadView()
@@ -91,7 +102,11 @@ struct TimelineThreadSummaryView: View {
private struct LoadingThreadView: View {
var body: some View {
ThreadView(senderID: "@alice:matrix.org", sender: nil, plainBody: "Hello world", formattedBody: nil)
ThreadView(senderID: "@alice:matrix.org",
sender: nil,
plainBody: "Hello world",
formattedBody: nil,
numberOfReplies: 42)
.redacted(reason: .placeholder)
}
}
@@ -103,12 +118,17 @@ struct TimelineThreadSummaryView: View {
let sender: TimelineItemSender?
let plainBody: String
let formattedBody: AttributedString?
let numberOfReplies: Int
var body: some View {
HStack(spacing: 8) {
HStack(spacing: 4) {
CompoundIcon(\.threads, size: .xSmall, relativeTo: .compound.bodyXS)
.foregroundColor(.compound.iconSecondary)
Text(L10n.commonReplies(numberOfReplies))
.font(.compound.bodyXSSemibold)
.foregroundColor(.compound.textPrimary)
LoadableAvatarImage(url: sender?.avatarURL,
name: sender?.displayName,
contentID: sender?.id,
@@ -121,13 +141,11 @@ struct TimelineThreadSummaryView: View {
.accessibilityLabel(L10n.commonInReplyTo(sender?.disambiguatedDisplayName ?? senderID))
Text(context.viewState.buildMessagePreview(formattedBody: formattedBody, plainBody: plainBody))
.multilineTextAlignment(.leading)
.font(.compound.bodyXS)
.foregroundColor(.compound.textSecondary)
.tint(.compound.textLinkExternal)
.lineLimit(2)
}
.padding(.vertical, 4.0)
.lineLimit(1)
.padding(.vertical, 7.0)
.padding(.horizontal, 8.0)
.background(Color.compound.bgSubtlePrimary)
.cornerRadius(8)
@@ -183,12 +201,14 @@ struct TimelineThreadSummaryView_Previews: PreviewProvider, TestablePreview {
TimelineThreadSummaryView(threadSummary: .error(message: "Error")),
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
latestEventContent: .message(.text(.init(body: "This is a threaded message"))))),
sender: .init(id: "@alice:matrix.org", displayName: "Alice McAliceFace"),
latestEventContent: .message(.text(.init(body: "This is a very long, multi-lined, threaded message"))),
numberOfReplies: 42)),
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
latestEventContent: .message(.notice(.init(body: "Hello world"))))),
latestEventContent: .message(.notice(.init(body: "Hello world"))),
numberOfReplies: 42)),
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
@@ -198,7 +218,8 @@ struct TimelineThreadSummaryView_Previews: PreviewProvider, TestablePreview {
waveform: nil,
source: nil,
fileSize: nil,
contentType: nil))))),
contentType: nil))),
numberOfReplies: 42)),
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
@@ -207,24 +228,28 @@ struct TimelineThreadSummaryView_Previews: PreviewProvider, TestablePreview {
source: nil,
fileSize: nil,
thumbnailSource: nil,
contentType: nil))))),
contentType: nil))),
numberOfReplies: 42)),
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
latestEventContent: .message(.image(.init(filename: "image.jpg",
caption: "Some image",
imageInfo: .mockImage,
thumbnailInfo: .mockThumbnail))))),
thumbnailInfo: .mockThumbnail))),
numberOfReplies: 42)),
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
latestEventContent: .message(.video(.init(filename: "video.mp4",
caption: "Some video",
videoInfo: .mockVideo,
thumbnailInfo: .mockVideoThumbnail))))),
thumbnailInfo: .mockVideoThumbnail))),
numberOfReplies: 42)),
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
latestEventContent: .message(.location(.init(body: ""))))),
latestEventContent: .message(.location(.init(body: ""))),
numberOfReplies: 42)),
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
@@ -234,34 +259,43 @@ struct TimelineThreadSummaryView_Previews: PreviewProvider, TestablePreview {
waveform: nil,
source: nil,
fileSize: nil,
contentType: nil))))),
contentType: nil))),
numberOfReplies: 42)),
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
latestEventContent: .poll(question: "Do you like polls?"))),
latestEventContent: .poll(question: "Do you like polls?"),
numberOfReplies: 42)),
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
latestEventContent: .redacted)),
latestEventContent: .redacted,
numberOfReplies: 42)),
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
latestEventContent: .message(.notice(.init(body: "", formattedBody: attributedStringWithMention))))),
latestEventContent: .message(.notice(.init(body: "", formattedBody: attributedStringWithMention))),
numberOfReplies: 42)),
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
latestEventContent: .message(.notice(.init(body: "", formattedBody: attributedStringWithAtRoomMention))))),
latestEventContent: .message(.notice(.init(body: "", formattedBody: attributedStringWithAtRoomMention))),
numberOfReplies: 42)),
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
latestEventContent: .message(.notice(.init(body: "", formattedBody: attributedStringWithRoomAliasMention))))),
latestEventContent: .message(.notice(.init(body: "", formattedBody: attributedStringWithRoomAliasMention))),
numberOfReplies: 42)),
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
latestEventContent: .message(.notice(.init(body: "", formattedBody: attributedStringWithRoomIDMention))))),
latestEventContent: .message(.notice(.init(body: "", formattedBody: attributedStringWithRoomIDMention))),
numberOfReplies: 42)),
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
latestEventContent: .message(.notice(.init(body: "", formattedBody: attributedStringWithEventOnRoomIDMention))))),
latestEventContent: .message(.notice(.init(body: "", formattedBody: attributedStringWithEventOnRoomIDMention))),
numberOfReplies: 42)),
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
latestEventContent: .message(.notice(.init(body: "", formattedBody: attributedStringWithEventOnRoomAliasMention)))))
latestEventContent: .message(.notice(.init(body: "", formattedBody: attributedStringWithEventOnRoomAliasMention))),
numberOfReplies: 42))
]
}

View File

@@ -21,6 +21,7 @@ enum EncryptionAuthenticity: Hashable {
case unverifiedIdentity(color: Color)
case verificationViolation(color: Color)
case sentInClear(color: Color)
case mismatchedSender(color: Color)
var message: String {
switch self {
@@ -36,6 +37,8 @@ enum EncryptionAuthenticity: Hashable {
L10n.eventShieldReasonPreviouslyVerified
case .sentInClear:
L10n.eventShieldReasonSentInClear
case .mismatchedSender:
L10n.eventShieldMismatchedSender
}
}
@@ -46,7 +49,8 @@ enum EncryptionAuthenticity: Hashable {
.unsignedDevice(let color),
.unverifiedIdentity(let color),
.verificationViolation(let color),
.sentInClear(let color):
.sentInClear(let color),
.mismatchedSender(let color):
color
}
}
@@ -54,7 +58,7 @@ enum EncryptionAuthenticity: Hashable {
var icon: KeyPath<CompoundIcons, Image> {
switch self {
case .notGuaranteed: \.info
case .unknownDevice, .unsignedDevice, .unverifiedIdentity, .verificationViolation: \.helpSolid
case .unknownDevice, .unsignedDevice, .unverifiedIdentity, .verificationViolation, .mismatchedSender: \.helpSolid
case .sentInClear: \.lockOff
}
}
@@ -86,6 +90,8 @@ extension EncryptionAuthenticity {
self = .verificationViolation(color: color)
case .sentInClear:
self = .sentInClear(color: color)
case .mismatchedSender:
self = .mismatchedSender(color: color)
}
}
}

View File

@@ -10,6 +10,6 @@ import MatrixRustSDK
enum TimelineItemThreadSummary: Hashable {
case notLoaded
case loading
case loaded(senderID: String, sender: TimelineItemSender, latestEventContent: TimelineEventContent)
case loaded(senderID: String, sender: TimelineItemSender, latestEventContent: TimelineEventContent, numberOfReplies: Int)
case error(message: String)
}

View File

@@ -699,7 +699,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
return .loaded(senderID: senderID,
sender: sender,
latestEventContent: latestEventContent)
latestEventContent: latestEventContent,
numberOfReplies: Int(threadSummary.numReplies()))
case .error(let message):
return .error(message: message)

View File

@@ -1,4 +1,5 @@
{
"originHash" : "f818e663a0525244e7213b08a1bf75d9223d516b797de9b2d90b511c62dedc99",
"pins" : [
{
"identity" : "swift-argument-parser",
@@ -22,10 +23,10 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/jpsim/Yams",
"state" : {
"revision" : "9281f8c99aff4f4a55dce22ae29b1181c935caa5",
"version" : "6.0.0"
"revision" : "7568d1c6c63a094405afb32264c57dc4e1435835",
"version" : "6.0.1"
}
}
],
"version" : 2
"version" : 3
}

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bfe8a376d3de35ee8c5c87dbc8c2e76485294cda575cb95f4a477d0533619caa
size 2557843
oid sha256:dd045932b14349f98ed11f488bf1dcc16e73a6f44432f004fa3466fe7cb7b6fc
size 2575000

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:797e94aa2584fd960f828e5e87c4207e1106e62be8254630a14406a5af7be6cf
size 2556128
oid sha256:eb8a5e3c36fabd52fddb0fa298c0fbf6145f832311df1bc5201bab20016450ce
size 2585179

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:50bb8dd3931574e4464321f2020a2f5f5e7603655bcac2cf0f9b1456c667f257
size 1273104
oid sha256:58c42cded5458253bf570df2661c849d4a69c76fb83b884d9642a5425233f7ec
size 1255297

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:614e39891312cdeab5caca4e6ab3d35773bd1df4edfa4eb3e137794a067667fe
size 1270109
oid sha256:9c1a75e26221738dc0ff905393f39aba7ab6629b36d777eb465c5b1b6bce957d
size 1251286

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:57e6b180bfb36b09a8037dbf51948e31aa14e11446beb80eb445a853bdb51924
size 204274
oid sha256:4adc90edca8173a1808ac32a7b7dc314d79d1bde7e27699791b730ad73418282
size 254752

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9cfd71844e9650e4dddb3ae402d4d9637932866ed5e7ce47be06ec504c594c2a
size 209066
oid sha256:88220246ed9669d2c46d51a1f470b7f74812a96185cad0618ffc6831d01bc2f3
size 285956

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f7f03c9bef8f1893c319839f3564695742b9e53ae4205a290082f6cbe2512375
size 142314
oid sha256:b494f40f2dd355953f8544e25f4acdafd639868060f61fb0172f9cc78c50e40c
size 170440

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f1aa50f59b5e3c3b1310ea0a196a4df4edb90ea9ab7ef8f4589c4962ee36cba6
size 141379
oid sha256:83a41a7bc90881e60bfe03b236db6c3c30a1b2217d25e21174e3553d1d240b87
size 190907

View File

@@ -65,7 +65,7 @@ packages:
# Element/Matrix dependencies
MatrixRustSDK:
url: https://github.com/element-hq/matrix-rust-components-swift
exactVersion: 25.06.12
exactVersion: 25.06.12-2
# path: ../matrix-rust-sdk
Compound:
url: https://github.com/element-hq/compound-ios