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_no_space_name" = "No space name";
"common_not_encrypted" = "Not encrypted"; "common_not_encrypted" = "Not encrypted";
"common_offline" = "Offline"; "common_offline" = "Offline";
"common_on_a_call" = "On a call";
"common_open_source_licenses" = "Open source licenses"; "common_open_source_licenses" = "Open source licenses";
"common_optic_id_ios" = "Optic ID"; "common_optic_id_ios" = "Optic ID";
"common_or" = "or"; "common_or" = "or";
@@ -294,6 +294,7 @@
"common_no_space_name" = "No space name"; "common_no_space_name" = "No space name";
"common_not_encrypted" = "Not encrypted"; "common_not_encrypted" = "Not encrypted";
"common_offline" = "Offline"; "common_offline" = "Offline";
"common_on_a_call" = "On a call";
"common_open_source_licenses" = "Open source licenses"; "common_open_source_licenses" = "Open source licenses";
"common_optic_id_ios" = "Optic ID"; "common_optic_id_ios" = "Optic ID";
"common_or" = "or"; "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") } internal static var commonNotEncrypted: String { return L10n.tr("Localizable", "common_not_encrypted") }
/// Offline /// Offline
internal static var commonOffline: String { return L10n.tr("Localizable", "common_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 /// Open source licenses
internal static var commonOpenSourceLicenses: String { return L10n.tr("Localizable", "common_open_source_licenses") } internal static var commonOpenSourceLicenses: String { return L10n.tr("Localizable", "common_open_source_licenses") }
/// Optic ID /// Optic ID
@@ -10045,6 +10045,11 @@ nonisolated class RoomMemberProxyMock: RoomMemberProxyProtocol, @unchecked Senda
nonisolated(unsafe) var displayName: String? nonisolated(unsafe) var displayName: String?
nonisolated(unsafe) var disambiguatedDisplayName: String? nonisolated(unsafe) var disambiguatedDisplayName: String?
nonisolated(unsafe) var avatarURL: URL? nonisolated(unsafe) var avatarURL: URL?
var status: UserStatus {
get { return underlyingStatus }
set(value) { underlyingStatus = value }
}
nonisolated(unsafe) var underlyingStatus: UserStatus!
var membership: MembershipState { var membership: MembershipState {
get { return underlyingMembership } get { return underlyingMembership }
set(value) { underlyingMembership = value } set(value) { underlyingMembership = value }
@@ -13,6 +13,7 @@ nonisolated struct RoomMemberProxyMockConfiguration {
var userID: String var userID: String
var displayName: String? var displayName: String?
var avatarURL: URL? var avatarURL: URL?
var status = UserStatus()
var membership: MembershipState var membership: MembershipState
var isIgnored = false var isIgnored = false
@@ -23,14 +24,12 @@ nonisolated struct RoomMemberProxyMockConfiguration {
extension RoomMemberProxyMock { extension RoomMemberProxyMock {
convenience init(with configuration: RoomMemberProxyMockConfiguration) { convenience init(with configuration: RoomMemberProxyMockConfiguration) {
self.init() self.init()
userID = configuration.userID userID = configuration.userID
displayName = configuration.displayName displayName = configuration.displayName
disambiguatedDisplayName = configuration.displayName.map { "\($0) (\(userID))" }
if let displayName = configuration.displayName {
disambiguatedDisplayName = "\(displayName) (\(userID))"
}
avatarURL = configuration.avatarURL avatarURL = configuration.avatarURL
status = configuration.status
membership = configuration.membership membership = configuration.membership
isIgnored = configuration.isIgnored isIgnored = configuration.isIgnored
@@ -93,6 +92,20 @@ extension RoomMemberProxyMock {
membership: .join)) 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 { static var mockVerbose: RoomMemberProxyMock {
RoomMemberProxyMock(with: .init(userID: "@charliev:matrix.org", RoomMemberProxyMock(with: .init(userID: "@charliev:matrix.org",
displayName: "Charlie is the best display name", displayName: "Charlie is the best display name",
+26 -1
View File
@@ -9,7 +9,6 @@
import Foundation import Foundation
extension UserProfile { extension UserProfile {
/// Mocks
static var mockAlice: UserProfile { static var mockAlice: UserProfile {
.init(userID: "@alice:matrix.org", displayName: "Alice", avatarURL: "mxc://matrix.org/UcCimidcvpFvWkPzvjXMQPHA") .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) .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 { static var mockVerbose: UserProfile {
.init(userID: "@charlie:matrix.org", displayName: "Charlie is the best display name", avatarURL: nil) .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 id: String
let name: String? let name: String?
let avatarURL: URL? let avatarURL: URL?
let status: UserStatus
let permalink: URL? let permalink: URL?
var isInvited: Bool var isInvited: Bool
@@ -34,6 +36,7 @@ nonisolated extension RoomMemberDetails {
id = proxy.userID id = proxy.userID
name = proxy.displayName name = proxy.displayName
avatarURL = proxy.avatarURL avatarURL = proxy.avatarURL
status = proxy.status
permalink = proxy.permalink permalink = proxy.permalink
isActive = proxy.isActive isActive = proxy.isActive
isInvited = proxy.membership == .invite isInvited = proxy.membership == .invite
@@ -36,6 +36,10 @@ final nonisolated class RoomMemberProxy: RoomMemberProxyProtocol {
member.avatarUrl.flatMap(URL.init(string:)) member.avatarUrl.flatMap(URL.init(string:))
} }
var status: UserStatus {
UserStatus()
}
var membership: MembershipState { var membership: MembershipState {
member.membership member.membership
} }
@@ -15,8 +15,8 @@ nonisolated protocol RoomMemberProxyProtocol: AnyObject, Sendable {
var displayName: String? { get } var displayName: String? { get }
var disambiguatedDisplayName: String? { get } var disambiguatedDisplayName: String? { get }
var avatarURL: URL? { get } var avatarURL: URL? { get }
var status: UserStatus { get }
var membership: MembershipState { get } var membership: MembershipState { get }
@@ -16,12 +16,18 @@ nonisolated struct TimelineItemSender: Identifiable, Hashable {
let displayName: String? let displayName: String?
let isDisplayNameAmbiguous: Bool let isDisplayNameAmbiguous: Bool
let avatarURL: URL? 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.id = id
self.displayName = displayName self.displayName = displayName
self.isDisplayNameAmbiguous = isDisplayNameAmbiguous self.isDisplayNameAmbiguous = isDisplayNameAmbiguous
self.avatarURL = avatarURL self.avatarURL = avatarURL
self.status = status
} }
init(senderID: String, senderProfile: ProfileDetails) { init(senderID: String, senderProfile: ProfileDetails) {
@@ -13,35 +13,41 @@ nonisolated struct UserProfile: Hashable, Identifiable {
let id: String let id: String
let displayName: String? let displayName: String?
let avatarURL: URL? 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 id = userID
self.displayName = displayName self.displayName = displayName
self.avatarURL = avatarURL self.avatarURL = avatarURL
self.status = status
} }
init(member: RoomMemberDetails) { init(member: RoomMemberDetails) {
id = member.id id = member.id
displayName = member.isBanned ? nil : member.name displayName = member.isBanned ? nil : member.name
avatarURL = member.isBanned ? nil : member.avatarURL avatarURL = member.isBanned ? nil : member.avatarURL
status = member.status
} }
init(sender: TimelineItemSender) { init(sender: TimelineItemSender) {
id = sender.id id = sender.id
displayName = sender.displayName displayName = sender.displayName
avatarURL = sender.avatarURL avatarURL = sender.avatarURL
status = sender.status
} }
init(sdkUserProfile: MatrixRustSDK.UserProfile) { init(sdkUserProfile: MatrixRustSDK.UserProfile) {
id = sdkUserProfile.userId id = sdkUserProfile.userId
displayName = sdkUserProfile.displayName displayName = sdkUserProfile.displayName
avatarURL = sdkUserProfile.avatarUrl.flatMap(URL.init(string:)) avatarURL = sdkUserProfile.avatarUrl.flatMap(URL.init(string:))
status = .init()
} }
init(sdkRoomHero: MatrixRustSDK.RoomHero) { init(sdkRoomHero: MatrixRustSDK.RoomHero) {
id = sdkRoomHero.userId id = sdkRoomHero.userId
displayName = sdkRoomHero.displayName displayName = sdkRoomHero.displayName
avatarURL = sdkRoomHero.avatarUrl.flatMap(URL.init(string:)) avatarURL = sdkRoomHero.avatarUrl.flatMap(URL.init(string:))
status = .init()
} }
init(member: RoomMemberProxyProtocol) { 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 { struct SearchUsersResults {
let results: [UserProfile] let results: [UserProfile]
let limited: Bool let limited: Bool