Add the UserStatus and a status field on UserProfile/RoomMember/TimelineItemSender.

This commit is contained in:
Doug
2026-07-10 12:40:14 +01:00
committed by Doug
parent 5743f263b6
commit 2c4775f357
11 changed files with 122 additions and 9 deletions
@@ -294,6 +294,7 @@
"common_no_space_name" = "No space name";
"common_not_encrypted" = "Not encrypted";
"common_offline" = "Offline";
"common_on_a_call" = "On a call";
"common_open_source_licenses" = "Open source licenses";
"common_optic_id_ios" = "Optic ID";
"common_or" = "or";
@@ -294,6 +294,7 @@
"common_no_space_name" = "No space name";
"common_not_encrypted" = "Not encrypted";
"common_offline" = "Offline";
"common_on_a_call" = "On a call";
"common_open_source_licenses" = "Open source licenses";
"common_optic_id_ios" = "Optic ID";
"common_or" = "or";
+2
View File
@@ -644,6 +644,8 @@ internal nonisolated enum L10n {
internal static var commonNotEncrypted: String { return L10n.tr("Localizable", "common_not_encrypted") }
/// Offline
internal static var commonOffline: String { return L10n.tr("Localizable", "common_offline") }
/// On a call
internal static var commonOnACall: String { return L10n.tr("Localizable", "common_on_a_call") }
/// Open source licenses
internal static var commonOpenSourceLicenses: String { return L10n.tr("Localizable", "common_open_source_licenses") }
/// Optic ID
@@ -10045,6 +10045,11 @@ nonisolated class RoomMemberProxyMock: RoomMemberProxyProtocol, @unchecked Senda
nonisolated(unsafe) var displayName: String?
nonisolated(unsafe) var disambiguatedDisplayName: String?
nonisolated(unsafe) var avatarURL: URL?
var status: UserStatus {
get { return underlyingStatus }
set(value) { underlyingStatus = value }
}
nonisolated(unsafe) var underlyingStatus: UserStatus!
var membership: MembershipState {
get { return underlyingMembership }
set(value) { underlyingMembership = value }
@@ -13,6 +13,7 @@ nonisolated struct RoomMemberProxyMockConfiguration {
var userID: String
var displayName: String?
var avatarURL: URL?
var status = UserStatus()
var membership: MembershipState
var isIgnored = false
@@ -23,14 +24,12 @@ nonisolated struct RoomMemberProxyMockConfiguration {
extension RoomMemberProxyMock {
convenience init(with configuration: RoomMemberProxyMockConfiguration) {
self.init()
userID = configuration.userID
displayName = configuration.displayName
if let displayName = configuration.displayName {
disambiguatedDisplayName = "\(displayName) (\(userID))"
}
disambiguatedDisplayName = configuration.displayName.map { "\($0) (\(userID))" }
avatarURL = configuration.avatarURL
status = configuration.status
membership = configuration.membership
isIgnored = configuration.isIgnored
@@ -93,6 +92,20 @@ extension RoomMemberProxyMock {
membership: .join))
}
static var mockErin: RoomMemberProxyMock {
RoomMemberProxyMock(with: .init(userID: "@erin:matrix.org",
displayName: "Erin",
status: .mockFocussing,
membership: .join))
}
static var mockFrank: RoomMemberProxyMock {
RoomMemberProxyMock(with: .init(userID: "@frank:matrix.org",
displayName: "Frank",
status: .mockCall,
membership: .join))
}
static var mockVerbose: RoomMemberProxyMock {
RoomMemberProxyMock(with: .init(userID: "@charliev:matrix.org",
displayName: "Charlie is the best display name",
+26 -1
View File
@@ -9,7 +9,6 @@
import Foundation
extension UserProfile {
/// Mocks
static var mockAlice: UserProfile {
.init(userID: "@alice:matrix.org", displayName: "Alice", avatarURL: "mxc://matrix.org/UcCimidcvpFvWkPzvjXMQPHA")
}
@@ -30,7 +29,33 @@ extension UserProfile {
.init(userID: "@dan:matrix.org", displayName: "Dan", avatarURL: .mockMXCUserAvatar)
}
static var mockErin: UserProfile {
.init(userID: "@erin:matrix.org", displayName: "Erin", status: .mockFocussing)
}
static var mockFrank: UserProfile {
.init(userID: "@frank:matrix.org", displayName: "Frank", status: .mockCall)
}
static var mockVerbose: UserProfile {
.init(userID: "@charlie:matrix.org", displayName: "Charlie is the best display name", avatarURL: nil)
}
}
nonisolated extension UserStatus {
static var mockCall: UserStatus {
.init(userSet: nil, call: .init(startDate: nil))
}
static var mockFocussing: UserStatus {
.init(userSet: .init(text: "Focussing", emoji: "🧑‍💻"), call: nil)
}
static var mockHoliday: UserStatus {
.init(userSet: .init(text: "Holiday", emoji: "🏝️"), call: nil)
}
static func mock(text: String, emoji: Character) -> UserStatus {
.init(userSet: .init(text: text, emoji: emoji), call: nil)
}
}
@@ -13,6 +13,8 @@ nonisolated struct RoomMemberDetails: Identifiable, Hashable {
let id: String
let name: String?
let avatarURL: URL?
let status: UserStatus
let permalink: URL?
var isInvited: Bool
@@ -34,6 +36,7 @@ nonisolated extension RoomMemberDetails {
id = proxy.userID
name = proxy.displayName
avatarURL = proxy.avatarURL
status = proxy.status
permalink = proxy.permalink
isActive = proxy.isActive
isInvited = proxy.membership == .invite
@@ -36,6 +36,10 @@ final nonisolated class RoomMemberProxy: RoomMemberProxyProtocol {
member.avatarUrl.flatMap(URL.init(string:))
}
var status: UserStatus {
UserStatus()
}
var membership: MembershipState {
member.membership
}
@@ -15,8 +15,8 @@ nonisolated protocol RoomMemberProxyProtocol: AnyObject, Sendable {
var displayName: String? { get }
var disambiguatedDisplayName: String? { get }
var avatarURL: URL? { get }
var status: UserStatus { get }
var membership: MembershipState { get }
@@ -16,12 +16,18 @@ nonisolated struct TimelineItemSender: Identifiable, Hashable {
let displayName: String?
let isDisplayNameAmbiguous: Bool
let avatarURL: URL?
let status: UserStatus
init(id: String, displayName: String? = nil, isDisplayNameAmbiguous: Bool = false, avatarURL: URL? = nil) {
init(id: String,
displayName: String? = nil,
isDisplayNameAmbiguous: Bool = false,
avatarURL: URL? = nil,
status: UserStatus = .init()) {
self.id = id
self.displayName = displayName
self.isDisplayNameAmbiguous = isDisplayNameAmbiguous
self.avatarURL = avatarURL
self.status = status
}
init(senderID: String, senderProfile: ProfileDetails) {
@@ -13,35 +13,41 @@ nonisolated struct UserProfile: Hashable, Identifiable {
let id: String
let displayName: String?
let avatarURL: URL?
let status: UserStatus
init(userID: String, displayName: String? = nil, avatarURL: URL? = nil) {
init(userID: String, displayName: String? = nil, avatarURL: URL? = nil, status: UserStatus = .init()) {
id = userID
self.displayName = displayName
self.avatarURL = avatarURL
self.status = status
}
init(member: RoomMemberDetails) {
id = member.id
displayName = member.isBanned ? nil : member.name
avatarURL = member.isBanned ? nil : member.avatarURL
status = member.status
}
init(sender: TimelineItemSender) {
id = sender.id
displayName = sender.displayName
avatarURL = sender.avatarURL
status = sender.status
}
init(sdkUserProfile: MatrixRustSDK.UserProfile) {
id = sdkUserProfile.userId
displayName = sdkUserProfile.displayName
avatarURL = sdkUserProfile.avatarUrl.flatMap(URL.init(string:))
status = .init()
}
init(sdkRoomHero: MatrixRustSDK.RoomHero) {
id = sdkRoomHero.userId
displayName = sdkRoomHero.displayName
avatarURL = sdkRoomHero.avatarUrl.flatMap(URL.init(string:))
status = .init()
}
init(member: RoomMemberProxyProtocol) {
@@ -55,6 +61,53 @@ nonisolated struct UserProfile: Hashable, Identifiable {
}
}
nonisolated struct UserStatus: Hashable {
/// The status manually set by the user (if any).
let userSet: UserSet?
/// The status automatically set by Element Call (while in a call).
let call: Call?
/// The status that should be displayed on the user's profile.
var displayed: DisplayedStatus? {
// The user-set status takes precedence over the call indicator.
userSet.map { .userSet($0) } ?? call.map { .inCall($0) }
}
nonisolated struct UserSet: Hashable {
let text: String
let emoji: Character
}
nonisolated struct Call: Hashable {
let startDate: Date?
}
nonisolated enum DisplayedStatus: Hashable {
case userSet(UserSet)
case inCall(Call)
var text: String {
switch self {
case .userSet(let userSet): userSet.text
case .inCall: L10n.commonOnACall
}
}
var emoji: Character {
switch self {
case .userSet(let userSet): userSet.emoji
case .inCall: "🎧"
}
}
}
}
nonisolated extension UserStatus {
init() {
self.init(userSet: nil, call: nil)
}
}
struct SearchUsersResults {
let results: [UserProfile]
let limited: Bool