mirror of
https://github.com/lux-tel/session-ios.git
synced 2026-10-30 20:38:32 +00:00
The `SessionThreadViewModel` has been renamed to `ConversationInfoViewModel` and is now a simple struct (as opposed to a FetchableRecord with a handful of behemoth database queries) The `ConversationInfoViewModel` and `MessageViewModel` now get initialised with a `ConversationDataCache` which is an object that holds a bunch of [ID -> Data] stores for various types that are needed to construct the aforementioned view models. The `ConversationDataCache` will generally be populated via the `ConversationDataHelper` which, when combined with our `ObservableEvent` system, is where the majority of the work for this refactoring has gone. This helper essentially has 4 parts: - `determineFetchRequirements`: Reviews any events which have come through and determines whether anything needs to be fetched from the database - `applyNonDatabaseEvents`: Updates the cached data directly based on delta information included in the `ObservableEvent` instances - `fetchFromDatabase`: Fetches any data from the database based on the info returned from `determineFetchRequirements`, this function will also resolve any dependant data that newly fetched objects need if they aren't already in the cache - `fetchFromLibSession`: Retrieves requested data from libSession Using the above setup all screens which contain conversation or message data now use these two view models, and can share the state of the view models between each other to reduce the need to refetch data when entering a new screen Some additional changes included due to the above are: • No longer need to pass `displayNameRetriever` throughout • Removed deprecated display name retrieval functions • Removed some duplicate code • Removed some synchronous database query hacks we needed until refactoring had been completed • Refactored mentions to be handled by tags • Refactored and optimised the searching logic (also added some caching) • Refactored the ThreadSettingsViewModel to use the new ObservationBuilder system • Refactored the ThreadPickerViewModel to use the new ObservationBuilder system • Refactored the HomeViewModel to retrieve data using the `ConversationDataHelper` • Refactored the ConversationViewModel to retrieve data using the `ConversationDataHelper` • Fixed an issue where communities weren't getting the correct "joined" timestamp
243 lines
11 KiB
Swift
243 lines
11 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import Foundation
|
|
import GRDB
|
|
import SessionUIKit
|
|
import SessionUtilitiesKit
|
|
|
|
public struct GroupMember: Sendable, Codable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
|
|
public static var databaseTableName: String { "groupMember" }
|
|
|
|
public typealias Columns = CodingKeys
|
|
public enum CodingKeys: String, CodingKey, ColumnExpression {
|
|
case groupId
|
|
case profileId
|
|
case role
|
|
case roleStatus
|
|
case isHidden
|
|
}
|
|
|
|
public enum Role: Int, Sendable, Codable, Comparable, CaseIterable, DatabaseValueConvertible {
|
|
case standard
|
|
case zombie
|
|
case moderator
|
|
case admin
|
|
|
|
public static func < (lhs: Role, rhs: Role) -> Bool { lhs.rawValue < rhs.rawValue }
|
|
}
|
|
|
|
public enum RoleStatus: Int, Sendable, Codable, CaseIterable, DatabaseValueConvertible {
|
|
case accepted
|
|
case pending
|
|
case failed
|
|
case notSentYet
|
|
case sending
|
|
case pendingRemoval
|
|
|
|
case unknown = 100
|
|
}
|
|
|
|
public let groupId: String
|
|
public let profileId: String
|
|
public let role: Role
|
|
public let roleStatus: RoleStatus
|
|
public let isHidden: Bool
|
|
|
|
// MARK: - Initialization
|
|
|
|
public init(
|
|
groupId: String,
|
|
profileId: String,
|
|
role: Role,
|
|
roleStatus: RoleStatus,
|
|
isHidden: Bool
|
|
) {
|
|
self.groupId = groupId
|
|
self.profileId = profileId
|
|
self.role = role
|
|
self.roleStatus = roleStatus
|
|
self.isHidden = isHidden
|
|
}
|
|
}
|
|
|
|
// MARK: - Decoding
|
|
|
|
extension GroupMember {
|
|
public init(from decoder: Decoder) throws {
|
|
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
|
|
|
|
self = GroupMember(
|
|
groupId: try container.decode(String.self, forKey: .groupId),
|
|
profileId: try container.decode(String.self, forKey: .profileId),
|
|
role: try container.decode(Role.self, forKey: .role),
|
|
// Added in `_018_GroupsRebuildChanges`
|
|
roleStatus: ((try? container.decode(RoleStatus.self, forKey: .roleStatus)) ?? .accepted),
|
|
// Added in `_006_FixHiddenModAdminSupport`
|
|
isHidden: ((try? container.decode(Bool.self, forKey: .isHidden)) ?? false)
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Convenience
|
|
|
|
public extension GroupMember {
|
|
var statusDescription: String? {
|
|
switch (role, roleStatus) {
|
|
case (_, .accepted): return nil // Nothing for "final" state
|
|
case (.zombie, _), (.moderator, _): return nil // Unused cases
|
|
case (.standard, .notSentYet): return "groupInviteNotSent".localized()
|
|
case (.standard, .sending): return "groupInviteSending".putNumber(1).localized()
|
|
case (.standard, .pending): return "groupInviteSent".localized()
|
|
case (.standard, .failed): return "groupInviteFailed".localized()
|
|
case (_, .pendingRemoval): return "groupPendingRemoval".localized()
|
|
case (.standard, .unknown): return "groupInviteStatusUnknown".localized()
|
|
case (.admin, .notSentYet): return "adminPromotionNotSent".localized()
|
|
case (.admin, .sending): return "adminSendingPromotion".putNumber(1).localized()
|
|
case (.admin, .pending): return "adminPromotionSent".localized()
|
|
case (.admin, .failed): return "adminPromotionFailed".localized()
|
|
case (.admin, .unknown): return "adminPromotionStatusUnknown".localized()
|
|
}
|
|
}
|
|
|
|
var statusDescriptionColor: ThemeValue {
|
|
switch (role, roleStatus) {
|
|
case (.zombie, _), (.moderator, _): return .textPrimary
|
|
case (_, .failed): return .danger
|
|
default: return .textPrimary
|
|
}
|
|
}
|
|
}
|
|
|
|
extension GroupMember: ProfileAssociated {
|
|
public var profileIcon: ProfilePictureView.Info.ProfileIcon {
|
|
switch role {
|
|
case .moderator, .admin: return .crown
|
|
default: return .none
|
|
}
|
|
}
|
|
|
|
public func itemDescription(using dependencies: Dependencies) -> String? { return statusDescription }
|
|
public func itemDescriptionColor(using dependencies: Dependencies) -> ThemeValue { return statusDescriptionColor }
|
|
|
|
public static func compareForManagement(
|
|
lhs: WithProfile<GroupMember>,
|
|
rhs: WithProfile<GroupMember>
|
|
) -> Bool {
|
|
let isUpdatedGroup: Bool = (((try? SessionId.Prefix(from: lhs.value.groupId)) ?? .group) == .group)
|
|
let lhsDisplayName: String = (lhs.profile?.displayName() ?? lhs.profileId.truncated())
|
|
let rhsDisplayName: String = (rhs.profile?.displayName() ?? rhs.profileId.truncated())
|
|
|
|
// Legacy groups have a different sorting behaviour
|
|
guard isUpdatedGroup else {
|
|
switch (lhs.value.role, rhs.value.role) {
|
|
case (.zombie, .standard), (.zombie, .moderator), (.zombie, .admin): return true
|
|
case (.standard, .zombie), (.moderator, .zombie), (.admin, .zombie): return false
|
|
default:
|
|
guard lhs.value.role == rhs.value.role else { return lhs.value.role < rhs.value.role }
|
|
|
|
return (lhsDisplayName.lowercased() < rhsDisplayName.lowercased())
|
|
}
|
|
}
|
|
|
|
/// We want to sort the member list so the most important info is at the top of the list, this means that we want to prioritise
|
|
/// • Invite failed, sorted as NameSortingOrder
|
|
/// • Invite not sent, sorted as NameSortingOrder
|
|
/// • Sending invite, sorted as NameSortingOrder
|
|
/// • Invite sent, sorted as NameSortingOrder
|
|
/// • Invite status unknown, sorted as NameSortingOrder
|
|
/// • Pending removal, sorted as NameSortingOrder
|
|
/// • Admin promotion failed, sorted as NameSortingOrder
|
|
/// • Admin promotion not sent, sorted as NameSortingOrder
|
|
/// • Sending admin promotion, sorted as NameSortingOrder
|
|
/// • Admin promotion sent, sorted as NameSortingOrder
|
|
/// • Admin promotion status unknown, sorted as NameSortingOrder
|
|
/// • Admin, sorted as NameSortingOrder
|
|
/// • Member, sorted as NameSortingOrder
|
|
///
|
|
/// And the current user should appear at the top of their respective group
|
|
let userSessionId: SessionId = lhs.currentUserSessionId
|
|
let desiredStatusOrder: [RoleStatus] = [
|
|
.failed, .notSentYet, .sending, .pending, .unknown, .pendingRemoval
|
|
]
|
|
|
|
/// If the role and status match then we want to sort by current user, no-name members by id, then by name
|
|
guard lhs.value.role != rhs.value.role || lhs.value.roleStatus != rhs.value.roleStatus else {
|
|
switch (lhs.profileId, rhs.profileId, lhs.profile?.name, rhs.profile?.name) {
|
|
case (userSessionId.hexString, userSessionId.hexString, _, _):
|
|
/// This case shouldn't be possible and is more to make the unit tests a bit nicer to read
|
|
return (lhsDisplayName.lowercased() < rhsDisplayName.lowercased())
|
|
case (userSessionId.hexString, _, _, _): return true
|
|
case (_, userSessionId.hexString, _, _): return false
|
|
case (_, _, .none, .some): return true
|
|
case (_, _, .some, .none): return false
|
|
case (_, _, .none, .none): return (lhsDisplayName.lowercased() < rhsDisplayName.lowercased())
|
|
case (_, _, .some, .some): return (lhsDisplayName.lowercased() < rhsDisplayName.lowercased())
|
|
}
|
|
}
|
|
|
|
switch (lhs.value.role, lhs.value.roleStatus, rhs.value.role, rhs.value.roleStatus) {
|
|
/// Non-accepted standard before admin
|
|
case (.standard, .failed, .admin, _), (.standard, .notSentYet, .admin, _),
|
|
(.standard, .sending, .admin, _), (.standard, .pending, .admin, _),
|
|
(.standard, .unknown, .admin, _), (.standard, .pendingRemoval, .admin, _):
|
|
return true
|
|
|
|
/// Non-accepted admin before accepted standard
|
|
case (.admin, .failed, .standard, .accepted), (.admin, .notSentYet, .standard, .accepted),
|
|
(.admin, .sending, .standard, .accepted), (.admin, .pending, .standard, .accepted),
|
|
(.admin, .unknown, .standard, .accepted), (.admin, .pendingRemoval, .standard, .accepted):
|
|
return true
|
|
|
|
/// Accepted admin before accepted standard
|
|
case (.admin, .accepted, .standard, .accepted): return true
|
|
|
|
/// Otherwise we should order based on the status position in `desiredStatusOrder`
|
|
default:
|
|
let lhsIndex = desiredStatusOrder.firstIndex(of: lhs.value.roleStatus)
|
|
let rhsIndex = desiredStatusOrder.firstIndex(of: rhs.value.roleStatus)
|
|
|
|
return ((lhsIndex ?? desiredStatusOrder.endIndex) < rhsIndex ?? desiredStatusOrder.endIndex)
|
|
}
|
|
}
|
|
|
|
public static func compare(
|
|
lhs: WithProfile<GroupMember>,
|
|
rhs: WithProfile<GroupMember>
|
|
) -> Bool {
|
|
let isUpdatedGroup: Bool = (((try? SessionId.Prefix(from: lhs.value.groupId)) ?? .group) == .group)
|
|
let lhsDisplayName: String = (lhs.profile?.displayName() ?? lhs.profileId.truncated())
|
|
let rhsDisplayName: String = (rhs.profile?.displayName() ?? rhs.profileId.truncated())
|
|
|
|
// Legacy groups have a different sorting behaviour
|
|
guard isUpdatedGroup else {
|
|
switch (lhs.value.role, rhs.value.role) {
|
|
case (.zombie, .standard), (.zombie, .moderator), (.zombie, .admin): return true
|
|
case (.standard, .zombie), (.moderator, .zombie), (.admin, .zombie): return false
|
|
default:
|
|
guard lhs.value.role == rhs.value.role else { return lhs.value.role < rhs.value.role }
|
|
|
|
return (lhsDisplayName.lowercased() < rhsDisplayName.lowercased())
|
|
}
|
|
}
|
|
|
|
/// We want to sort the member list so the most important info is at the top of the list, this means that we want to prioritise
|
|
/// • Current user
|
|
/// • Admin, sorted as NameSortingOrder
|
|
/// • Member, sorted as NameSortingOrder
|
|
let userSessionId: SessionId = lhs.currentUserSessionId
|
|
|
|
switch (lhs.profileId, rhs.profileId, lhs.value.role, rhs.value.role) {
|
|
/// Current user first
|
|
case (userSessionId.hexString, _, _, _): return true
|
|
case (_, userSessionId.hexString, _, _): return false
|
|
|
|
/// Admin before standard
|
|
case (_, _, .admin, .standard): return true
|
|
case (_, _, .standard, .admin): return false
|
|
|
|
/// Otherwise we should sort by NameSortingOrder
|
|
default: return (lhsDisplayName.lowercased() < rhsDisplayName.lowercased())
|
|
}
|
|
}
|
|
}
|