Show the last message state in the room list.

This commit is contained in:
Doug
2025-11-27 13:33:28 +00:00
committed by Doug
parent dc8f5ef5a9
commit 62930968f8
18 changed files with 129 additions and 15 deletions
+2
View File
@@ -524,6 +524,8 @@ internal enum L10n {
internal static var commonMessage: String { return L10n.tr("Localizable", "common_message") }
/// Message actions
internal static var commonMessageActions: String { return L10n.tr("Localizable", "common_message_actions") }
/// Message failed to send
internal static var commonMessageFailedToSend: String { return L10n.tr("Localizable", "common_message_failed_to_send") }
/// Message layout
internal static var commonMessageLayout: String { return L10n.tr("Localizable", "common_message_layout") }
/// Message removed
@@ -83,6 +83,7 @@ extension RoomSummary {
activeMembersCount: 0,
lastMessage: AttributedString("I do not wish to take the trouble to understand mysticism"),
lastMessageDate: .mock,
lastMessageState: nil,
unreadMessagesCount: 0,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
@@ -109,6 +110,7 @@ extension Array where Element == RoomSummary {
activeMembersCount: 0,
lastMessage: AttributedString("I do not wish to take the trouble to understand mysticism"),
lastMessageDate: .mock,
lastMessageState: nil,
unreadMessagesCount: 0,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
@@ -130,6 +132,7 @@ extension Array where Element == RoomSummary {
activeMembersCount: 0,
lastMessage: AttributedString("How do you see the Emperor then? You think he keeps office hours?"),
lastMessageDate: .mock,
lastMessageState: nil,
unreadMessagesCount: 2,
unreadMentionsCount: 0,
unreadNotificationsCount: 2,
@@ -151,6 +154,7 @@ extension Array where Element == RoomSummary {
activeMembersCount: 0,
lastMessage: try? AttributedString(markdown: "He certainly seemed no *mental genius* to me"),
lastMessageDate: .mock,
lastMessageState: nil,
unreadMessagesCount: 3,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
@@ -172,6 +176,7 @@ extension Array where Element == RoomSummary {
activeMembersCount: 0,
lastMessage: AttributedString("There's an archaic measure of time that's called the month"),
lastMessageDate: .mock,
lastMessageState: nil,
unreadMessagesCount: 2,
unreadMentionsCount: 2,
unreadNotificationsCount: 2,
@@ -193,6 +198,7 @@ extension Array where Element == RoomSummary {
activeMembersCount: 0,
lastMessage: AttributedString("Clearly, if Earth is powerful enough to do that, it might also be capable of adjusting minds in order to force belief in its radioactivity"),
lastMessageDate: .mock,
lastMessageState: nil,
unreadMessagesCount: 1,
unreadMentionsCount: 1,
unreadNotificationsCount: 1,
@@ -214,6 +220,7 @@ extension Array where Element == RoomSummary {
activeMembersCount: 0,
lastMessage: AttributedString("Are you groping for the word 'paranoia'?"),
lastMessageDate: .mock,
lastMessageState: nil,
unreadMessagesCount: 6,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
@@ -235,6 +242,7 @@ extension Array where Element == RoomSummary {
activeMembersCount: 0,
lastMessage: nil,
lastMessageDate: .mock,
lastMessageState: nil,
unreadMessagesCount: 1,
unreadMentionsCount: 0,
unreadNotificationsCount: 1,
@@ -256,6 +264,7 @@ extension Array where Element == RoomSummary {
activeMembersCount: 0,
lastMessage: nil,
lastMessageDate: nil,
lastMessageState: nil,
unreadMessagesCount: 0,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
@@ -310,6 +319,7 @@ extension Array where Element == RoomSummary {
activeMembersCount: 0,
lastMessage: nil,
lastMessageDate: nil,
lastMessageState: nil,
unreadMessagesCount: 0,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
@@ -331,6 +341,7 @@ extension Array where Element == RoomSummary {
activeMembersCount: 0,
lastMessage: nil,
lastMessageDate: nil,
lastMessageState: nil,
unreadMessagesCount: 0,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
@@ -355,6 +366,7 @@ extension Array where Element == RoomSummary {
activeMembersCount: 0,
lastMessage: nil,
lastMessageDate: nil,
lastMessageState: nil,
unreadMessagesCount: 0,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
@@ -376,6 +388,7 @@ extension Array where Element == RoomSummary {
activeMembersCount: 0,
lastMessage: nil,
lastMessageDate: nil,
lastMessageState: nil,
unreadMessagesCount: 0,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
@@ -197,6 +197,9 @@ struct HomeScreenRoom: Identifiable, Equatable {
let lastMessage: AttributedString?
enum LastMessageState { case sending, failed }
let lastMessageState: LastMessageState?
let avatar: RoomAvatar
let canonicalAlias: String?
@@ -204,11 +207,13 @@ struct HomeScreenRoom: Identifiable, Equatable {
let isTombstoned: Bool
var displayedLastMessage: AttributedString? {
// If the room is tombstoned, show a specific message, regardless of any last message.
guard !isTombstoned else {
return AttributedString(L10n.screenRoomlistTombstonedRoomDescription)
if isTombstoned {
AttributedString(L10n.screenRoomlistTombstonedRoomDescription)
} else if lastMessageState == .failed {
AttributedString(L10n.commonMessageFailedToSend)
} else {
lastMessage
}
return lastMessage
}
static func placeholder() -> HomeScreenRoom {
@@ -222,6 +227,7 @@ struct HomeScreenRoom: Identifiable, Equatable {
isFavourite: false,
timestamp: "Now",
lastMessage: placeholderLastMessage,
lastMessageState: nil,
avatar: .room(id: "", name: "", avatarURL: nil),
canonicalAlias: nil,
isTombstoned: false)
@@ -260,8 +266,23 @@ extension HomeScreenRoom {
isFavourite: summary.isFavourite,
timestamp: summary.lastMessageDate?.formattedMinimal(),
lastMessage: summary.lastMessage,
lastMessageState: summary.homeScreenLastMessageState,
avatar: summary.avatar,
canonicalAlias: summary.canonicalAlias,
isTombstoned: summary.isTombstoned)
}
}
private extension RoomSummary {
var homeScreenLastMessageState: HomeScreenRoom.LastMessageState? {
if isTombstoned {
nil
} else {
switch lastMessageState {
case .sending: .sending
case .failed: .failed
case .none: .none
}
}
}
}
@@ -223,6 +223,7 @@ private extension HomeScreenRoom {
activeMembersCount: 0,
lastMessage: nil,
lastMessageDate: nil,
lastMessageState: nil,
unreadMessagesCount: 0,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
@@ -257,6 +258,7 @@ private extension HomeScreenRoom {
activeMembersCount: 0,
lastMessage: nil,
lastMessageDate: nil,
lastMessageState: nil,
unreadMessagesCount: 0,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
@@ -156,6 +156,7 @@ private extension HomeScreenRoom {
activeMembersCount: 0,
lastMessage: nil,
lastMessageDate: nil,
lastMessageState: nil,
unreadMessagesCount: 0,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
@@ -187,6 +188,7 @@ private extension HomeScreenRoom {
activeMembersCount: 0,
lastMessage: nil,
lastMessageDate: nil,
lastMessageState: nil,
unreadMessagesCount: 0,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
@@ -79,11 +79,24 @@ struct HomeScreenRoomCell: View {
@ViewBuilder
private var header: some View {
HStack(alignment: .top, spacing: 16) {
Text(room.name)
.font(.compound.bodyLGSemibold)
.foregroundColor(.compound.textPrimary)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
HStack(spacing: 4) {
Text(room.name)
.font(.compound.bodyLGSemibold)
.foregroundColor(.compound.textPrimary)
.lineLimit(1)
switch room.lastMessageState {
case .sending:
CompoundIcon(\.time, size: .xSmall, relativeTo: .compound.bodyLGSemibold)
.foregroundStyle(.compound.iconTertiary)
case .failed:
CompoundIcon(\.errorSolid, size: .xSmall, relativeTo: .compound.bodyLGSemibold)
.foregroundStyle(.compound.iconCriticalPrimary)
case .none:
EmptyView()
}
}
.frame(maxWidth: .infinity, alignment: .leading)
if let timestamp = room.timestamp {
Text(timestamp)
@@ -99,7 +112,7 @@ struct HomeScreenRoomCell: View {
ZStack(alignment: .topLeading) {
// Hidden text with 2 lines to maintain consistent height, scaling with dynamic text.
Text(" \n ")
.lastMessageFormatting()
.lastMessageFormatting(hasFailed: false)
.hidden()
.environment(\.redactionReasons, []) // Always maintain consistent height
@@ -142,7 +155,7 @@ struct HomeScreenRoomCell: View {
private var lastMessage: some View {
if let displayedLastMessage = room.displayedLastMessage {
Text(displayedLastMessage)
.lastMessageFormatting()
.lastMessageFormatting(hasFailed: room.lastMessageState == .failed)
}
}
}
@@ -159,9 +172,9 @@ struct HomeScreenRoomCellButtonStyle: ButtonStyle {
}
private extension View {
func lastMessageFormatting() -> some View {
func lastMessageFormatting(hasFailed: Bool) -> some View {
font(.compound.bodyMD)
.foregroundColor(.compound.textSecondary)
.foregroundColor(hasFailed ? .compound.textCriticalPrimary : .compound.textSecondary)
.lineLimit(2)
.multilineTextAlignment(.leading)
}
@@ -174,6 +187,8 @@ struct HomeScreenRoomCell_Previews: PreviewProvider, TestablePreview {
static let summaryProviderForNotificationsState = RoomSummaryProviderMock(.init(state: .loaded(.mockRoomsWithNotificationsState)))
static let notificationsStateRooms = summaryProviderForNotificationsState.roomListPublisher.value.compactMap(mockRoom)
static let lastMessageStateRooms = [makeRoom(lastMessageState: .sending), makeRoom(lastMessageState: .failed)]
static var previews: some View {
VStack(spacing: 0) {
ForEach(genericRooms) { room in
@@ -192,6 +207,14 @@ struct HomeScreenRoomCell_Previews: PreviewProvider, TestablePreview {
}
.previewLayout(.sizeThatFits)
.previewDisplayName("Notifications State")
VStack(spacing: 0) {
ForEach(lastMessageStateRooms) { room in
HomeScreenRoomCell(room: room, isSelected: false, mediaProvider: MediaProviderMock(configuration: .init())) { _ in }
}
}
.previewLayout(.sizeThatFits)
.previewDisplayName("Last Message State")
}
static func mockRoom(summary: RoomSummary) -> HomeScreenRoom? {
@@ -208,4 +231,31 @@ struct HomeScreenRoomCell_Previews: PreviewProvider, TestablePreview {
notificationManager: NotificationManagerMock(),
userIndicatorController: ServiceLocator.shared.userIndicatorController)
}
static func makeRoom(lastMessageState: RoomSummary.LastMessageState) -> HomeScreenRoom {
let summary = RoomSummary(room: RoomSDKMock(),
id: UUID().uuidString,
joinRequestType: nil,
name: "Foundation and Empire",
isDirect: false,
isSpace: false,
avatarURL: .mockMXCAvatar,
heroes: [],
activeMembersCount: 0,
lastMessage: AttributedString("How do you see the Emperor then? You think he keeps office hours?"),
lastMessageDate: .mock,
lastMessageState: lastMessageState,
unreadMessagesCount: 2,
unreadMentionsCount: 0,
unreadNotificationsCount: 2,
notificationMode: .mute,
canonicalAlias: "#foundation-and-empire:matrix.org",
alternativeAliases: [],
hasOngoingCall: false,
isMarkedUnread: false,
isFavourite: false,
isTombstoned: false)
return .init(summary: summary, hideUnreadMessagesBadge: false)
}
}
@@ -29,6 +29,8 @@ struct RoomSummary {
}
}
}
enum LastMessageState { case sending, failed }
let room: Room
@@ -46,6 +48,7 @@ struct RoomSummary {
let lastMessage: AttributedString?
let lastMessageDate: Date?
let lastMessageState: LastMessageState?
let unreadMessagesCount: UInt
let unreadMentionsCount: UInt
let unreadNotificationsCount: UInt
@@ -117,6 +120,7 @@ extension RoomSummary {
lastMessage = AttributedString(string)
lastMessageDate = .mock
lastMessageState = nil
unreadMessagesCount = hasUnreadMessages ? 1 : 0
unreadMentionsCount = hasUnreadMentions ? 1 : 0
unreadNotificationsCount = hasUnreadNotifications ? 1 : 0
@@ -261,13 +261,15 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
var attributedLastMessage: AttributedString?
var lastMessageDate: Date?
var lastMessageState: RoomSummary.LastMessageState?
if let latestRoomMessage = roomDetails.latestEvent {
switch latestRoomMessage {
case .local(let timestamp, let senderID, let profile, let content, _):
case .local(let timestamp, let senderID, let profile, let content, let isSending):
let sender = TimelineItemSender(senderID: senderID, senderProfile: profile)
attributedLastMessage = eventStringBuilder.buildAttributedString(for: content, sender: sender, isOutgoing: true)
lastMessageDate = Date(timeIntervalSince1970: TimeInterval(timestamp / 1000))
lastMessageState = isSending ? .sending : .failed // No need to worry about sent for .local: https://github.com/matrix-org/matrix-rust-sdk/issues/3941
case .remote(let timestamp, let senderID, let isOwn, let profile, let content):
let sender = TimelineItemSender(senderID: senderID, senderProfile: profile)
attributedLastMessage = eventStringBuilder.buildAttributedString(for: content, sender: sender, isOutgoing: isOwn)
@@ -301,6 +303,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
activeMembersCount: UInt(roomInfo.activeMembersCount),
lastMessage: attributedLastMessage,
lastMessageDate: lastMessageDate,
lastMessageState: lastMessageState,
unreadMessagesCount: UInt(roomInfo.numUnreadMessages),
unreadMentionsCount: UInt(roomInfo.numUnreadMentions),
unreadNotificationsCount: UInt(roomInfo.numUnreadNotifications),