From e80717f63d8d55c6dec0fc11fcaed6cd47b561b3 Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 10 Jul 2026 13:16:03 +0100 Subject: [PATCH] Show user statuses in mention suggestions. --- .../CompletionSuggestionService.swift | 3 +- .../CompletionSuggestionServiceProtocol.swift | 22 ++++-- .../View/CompletionSuggestionView.swift | 15 +++- .../View/MentionSuggestionItemView.swift | 69 ++++++++++++++++--- 4 files changed, 91 insertions(+), 18 deletions(-) diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionService.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionService.swift index bb20b60b4..a647b955a 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionService.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionService.swift @@ -87,7 +87,8 @@ final class CompletionSuggestionService: CompletionSuggestionServiceProtocol { Self.shouldIncludeMember(userID: member.userID, displayName: member.displayName, searchText: suggestionTrigger.text) else { return nil } - return .init(suggestionType: .user(.init(id: member.userID, displayName: member.displayName, avatarURL: member.avatarURL)), range: suggestionTrigger.range, rawSuggestionText: suggestionTrigger.text) + return .init(suggestionType: .user(.init(id: member.userID, displayName: member.displayName, avatarURL: member.avatarURL, status: member.status)), + range: suggestionTrigger.range, rawSuggestionText: suggestionTrigger.text) } if canMentionAllUsers, diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionServiceProtocol.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionServiceProtocol.swift index 9e7ef6c67..3ec305a37 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionServiceProtocol.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionServiceProtocol.swift @@ -32,6 +32,7 @@ struct SuggestionItem: Identifiable, Equatable { let id: String let displayName: String? let avatarURL: URL? + var status = UserStatus() } struct Room: Equatable { @@ -59,22 +60,31 @@ struct SuggestionItem: Identifiable, Equatable { var displayName: String { switch suggestionType { case .allUsers: - return PillUtilities.everyone + PillUtilities.everyone case .user(let user): - return user.displayName ?? user.id + user.displayName ?? user.id case .room(let room): - return room.name + room.name + } + } + + var statusEmoji: Character? { + switch suggestionType { + case .user(let user): + user.status.displayed?.emoji + case .allUsers, .room: + nil } } var subtitle: String? { switch suggestionType { case .allUsers: - return PillUtilities.atRoom + PillUtilities.atRoom case .user(let user): - return user.displayName == nil ? nil : user.id + user.displayName == nil ? nil : user.id case .room(let room): - return room.canonicalAlias + room.canonicalAlias } } } diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/CompletionSuggestionView.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/CompletionSuggestionView.swift index ea170fc16..d385a4866 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/CompletionSuggestionView.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/CompletionSuggestionView.swift @@ -110,8 +110,19 @@ struct CompletionSuggestion_Previews: PreviewProvider, TestablePreview { // Putting them is VStack allows the preview to work properly in tests VStack(spacing: 8) { CompletionSuggestionView(mediaProvider: MediaProviderMock(.init()), - items: [.init(suggestionType: .user(.init(id: "@user_mention_1:matrix.org", displayName: "User 1", avatarURL: nil)), range: .init(), rawSuggestionText: ""), - .init(suggestionType: .user(.init(id: "@user_mention_2:matrix.org", displayName: "User 2", avatarURL: .mockMXCUserAvatar)), range: .init(), rawSuggestionText: "")]) { _ in } + items: [ + .init(suggestionType: .user(.init(id: "@user_mention_1:matrix.org", + displayName: "User 1", + avatarURL: nil)), + range: .init(), + rawSuggestionText: ""), + .init(suggestionType: .user(.init(id: "@user_mention_2:matrix.org", + displayName: "User 2", + avatarURL: .mockMXCUserAvatar, + status: .mockCall)), + range: .init(), + rawSuggestionText: "") + ]) { _ in } } VStack(spacing: 8) { diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MentionSuggestionItemView.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MentionSuggestionItemView.swift index a5240e1be..78140f7cb 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MentionSuggestionItemView.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MentionSuggestionItemView.swift @@ -6,6 +6,7 @@ // Please see LICENSE files in the repository root for full details. // +import Compound import SwiftUI struct MentionSuggestionItemView: View { @@ -13,19 +14,29 @@ struct MentionSuggestionItemView: View { let item: SuggestionItem var body: some View { - HStack(alignment: .center, spacing: 16) { + HStack(alignment: .center, spacing: 12) { avatar .accessibilityHidden(true) + VStack(alignment: .leading, spacing: 0) { - Text(item.displayName) - .font(.compound.bodyLG) - .foregroundColor(.compound.textPrimary) - .lineLimit(1) + HStack(spacing: 2) { + Text(item.displayName) + .lineLimit(1) + .padding(.vertical, 1) // Compensation for Figma line height. + + if let statusEmoji = item.statusEmoji { + Text(String(statusEmoji)) + } + } + .font(.compound.bodyLG) + .foregroundStyle(.compound.textPrimary) + if let subtitle = item.subtitle { Text(subtitle) .font(.compound.bodySM) - .foregroundColor(.compound.textSecondary) + .foregroundStyle(.compound.textSecondary) .lineLimit(1) + .padding(.vertical, 1) // Compensation for Figma line height. } } } @@ -49,12 +60,52 @@ struct MentionSuggestionItemView_Previews: PreviewProvider, TestablePreview { static let mockMediaProvider = MediaProviderMock(.init()) static var previews: some View { - MentionSuggestionItemView(mediaProvider: mockMediaProvider, item: .init(suggestionType: .user(.init(id: "test", displayName: "Test", avatarURL: .mockMXCUserAvatar)), range: .init(), rawSuggestionText: "")) + MentionSuggestionItemView(mediaProvider: mockMediaProvider, + item: .init(suggestionType: .user(.init(id: "test", displayName: "Test", avatarURL: .mockMXCUserAvatar)), + range: .init(), + rawSuggestionText: "")) .previewDisplayName("User") - MentionSuggestionItemView(mediaProvider: mockMediaProvider, item: .init(suggestionType: .user(.init(id: "test2", displayName: nil, avatarURL: nil)), range: .init(), rawSuggestionText: "")) + + VStack(alignment: .leading, spacing: 8) { + MentionSuggestionItemView(mediaProvider: mockMediaProvider, + item: .init(suggestionType: .user(.init(id: "@john.smith:example.com", + displayName: "John Smith", + avatarURL: nil, + status: .mockHoliday)), + range: .init(), + rawSuggestionText: "")) + + MentionSuggestionItemView(mediaProvider: mockMediaProvider, + item: .init(suggestionType: .user(.init(id: "@alice.liddel:example.com", + displayName: "Alice Liddell", + avatarURL: nil, + status: .mockCall)), + range: .init(), + rawSuggestionText: "")) + + MentionSuggestionItemView(mediaProvider: mockMediaProvider, + item: .init(suggestionType: .user(.init(id: "@long.name:example.com", + displayName: "I have a long name that doesn't fit on one line", + avatarURL: nil, + status: .mock(text: "Travelling", emoji: "🚆"))), + range: .init(), + rawSuggestionText: "")) + } + .padding(16) + .previewDisplayName("User with status") + + MentionSuggestionItemView(mediaProvider: mockMediaProvider, + item: .init(suggestionType: .user(.init(id: "test2", displayName: nil, avatarURL: nil)), + range: .init(), + rawSuggestionText: "")) .previewDisplayName("User no display name") - MentionSuggestionItemView(mediaProvider: mockMediaProvider, item: .init(suggestionType: .allUsers(.room(id: "room", name: "Room", avatarURL: .mockMXCAvatar)), range: .init(), rawSuggestionText: "")) + + MentionSuggestionItemView(mediaProvider: mockMediaProvider, + item: .init(suggestionType: .allUsers(.room(id: "room", name: "Room", avatarURL: .mockMXCAvatar)), + range: .init(), + rawSuggestionText: "")) .previewDisplayName("All users") + MentionSuggestionItemView(mediaProvider: mockMediaProvider, item: .init(suggestionType: .room(.init(id: "room", canonicalAlias: "#room:matrix.org",