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
408 lines
19 KiB
Swift
408 lines
19 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import Foundation
|
|
import Combine
|
|
import GRDB
|
|
import DifferenceKit
|
|
import SessionUIKit
|
|
import SessionMessagingKit
|
|
import SessionUtilitiesKit
|
|
import SessionNetworkingKit
|
|
|
|
class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTableSource {
|
|
typealias TableItem = String
|
|
|
|
public let dependencies: Dependencies
|
|
public let navigatableState: NavigatableState = NavigatableState()
|
|
public let state: TableDataState<Section, TableItem> = TableDataState()
|
|
public let observableState: ObservableTableSourceState<Section, TableItem> = ObservableTableSourceState()
|
|
|
|
private let threadId: String
|
|
private let threadVariant: SessionThread.Variant
|
|
private var isNoteToSelf: Bool
|
|
private let currentUserRole: GroupMember.Role?
|
|
private let originalConfig: DisappearingMessagesConfiguration
|
|
private var configSubject: CurrentValueSubject<DisappearingMessagesConfiguration, Never>
|
|
|
|
// MARK: - Initialization
|
|
|
|
init(
|
|
threadId: String,
|
|
threadVariant: SessionThread.Variant,
|
|
currentUserRole: GroupMember.Role?,
|
|
config: DisappearingMessagesConfiguration,
|
|
using dependencies: Dependencies
|
|
) {
|
|
self.dependencies = dependencies
|
|
self.threadId = threadId
|
|
self.threadVariant = threadVariant
|
|
self.isNoteToSelf = (threadId == dependencies[cache: .general].sessionId.hexString)
|
|
self.currentUserRole = currentUserRole
|
|
self.originalConfig = config
|
|
self.configSubject = CurrentValueSubject(config)
|
|
}
|
|
|
|
// MARK: - Config
|
|
|
|
public enum Section: SessionTableSection {
|
|
case type
|
|
case timerLegacy
|
|
case timerDisappearAfterSend
|
|
case timerDisappearAfterRead
|
|
case noteToSelf
|
|
case group
|
|
|
|
var title: String? {
|
|
switch self {
|
|
case .type: return "disappearingMessagesDeleteType".localized()
|
|
// We need to keep these although the titles of them are the same
|
|
// because we need them to trigger timer section to refresh when
|
|
// the user selects different disappearing messages type
|
|
case .timerLegacy, .timerDisappearAfterSend, .timerDisappearAfterRead: return "disappearingMessagesTimer".localized()
|
|
case .noteToSelf: return nil
|
|
case .group: return nil
|
|
}
|
|
}
|
|
|
|
var style: SessionTableSectionStyle { return .titleRoundedContent }
|
|
|
|
var footer: String? {
|
|
switch self {
|
|
case .group:
|
|
return "\("disappearingMessagesDescription".localized())\n\("disappearingMessagesOnlyAdmins".localized())"
|
|
default: return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Content
|
|
|
|
let title: String = "disappearingMessages".localized()
|
|
lazy var subtitle: String? = {
|
|
switch (threadVariant, isNoteToSelf) {
|
|
case (.contact, false): return "disappearingMessagesDescription1".localized()
|
|
case (.group, _), (.legacyGroup, _): return "disappearingMessagesDisappearAfterSendDescription".localized()
|
|
case (.community, _): return nil
|
|
case (_, true): return "disappearingMessagesDescription".localized()
|
|
}
|
|
}()
|
|
|
|
lazy var footerButtonInfo: AnyPublisher<SessionButton.Info?, Never> = configSubject
|
|
.map { [originalConfig] currentConfig -> Bool in
|
|
// Need to explicitly compare values because 'lastChangeTimestampMs' will differ
|
|
return (
|
|
currentConfig.isEnabled != originalConfig.isEnabled ||
|
|
currentConfig.durationSeconds != originalConfig.durationSeconds ||
|
|
currentConfig.type != originalConfig.type
|
|
)
|
|
}
|
|
.removeDuplicates()
|
|
.map { [weak self] shouldShowConfirmButton -> SessionButton.Info? in
|
|
guard shouldShowConfirmButton else { return nil }
|
|
|
|
return SessionButton.Info(
|
|
style: .bordered,
|
|
title: "set".localized(),
|
|
isEnabled: true,
|
|
accessibility: Accessibility(
|
|
identifier: "Set button",
|
|
label: "Set button"
|
|
),
|
|
minWidth: 110,
|
|
onTap: {
|
|
self?.saveChanges()
|
|
self?.dismissScreen()
|
|
}
|
|
)
|
|
}
|
|
.eraseToAnyPublisher()
|
|
|
|
lazy var observation: TargetObservation = ObservationBuilderOld
|
|
.subject(configSubject)
|
|
.compactMap { [weak self] currentConfig -> [SectionModel]? in self?.content(currentConfig) }
|
|
|
|
private func content(_ currentConfig: DisappearingMessagesConfiguration) -> [SectionModel] {
|
|
switch (threadVariant, isNoteToSelf) {
|
|
case (.contact, false):
|
|
return [
|
|
SectionModel(
|
|
model: .type,
|
|
elements: [
|
|
SessionCell.Info(
|
|
id: "off".localized(),
|
|
title: "off".localized(),
|
|
trailingAccessory: .radio(
|
|
isSelected: !currentConfig.isEnabled,
|
|
accessibility: Accessibility(
|
|
identifier: "Off - Radio"
|
|
)
|
|
),
|
|
accessibility: Accessibility(
|
|
identifier: "Disable disappearing messages (Off option)",
|
|
label: "Disable disappearing messages (Off option)"
|
|
),
|
|
onTap: { [weak self] in
|
|
self?.configSubject.send(
|
|
currentConfig.with(
|
|
isEnabled: false,
|
|
durationSeconds: DisappearingMessagesConfiguration.DefaultDuration.off.seconds
|
|
)
|
|
)
|
|
}
|
|
),
|
|
SessionCell.Info(
|
|
id: "disappearingMessagesDisappearAfterRead".localized(),
|
|
title: "disappearingMessagesDisappearAfterRead".localized(),
|
|
subtitle: "disappearingMessagesDisappearAfterReadDescription".localized(),
|
|
trailingAccessory: .radio(
|
|
isSelected: (
|
|
currentConfig.isEnabled &&
|
|
currentConfig.type == .disappearAfterRead
|
|
),
|
|
accessibility: Accessibility(
|
|
identifier: "Disappear After Read - Radio"
|
|
)
|
|
),
|
|
accessibility: Accessibility(
|
|
identifier: "Disappear after read option",
|
|
label: "Disappear after read option"
|
|
),
|
|
onTap: { [weak self, originalConfig] in
|
|
switch (originalConfig.isEnabled, originalConfig.type) {
|
|
case (true, .disappearAfterRead): self?.configSubject.send(originalConfig)
|
|
default: self?.configSubject.send(
|
|
currentConfig.with(
|
|
isEnabled: true,
|
|
durationSeconds: DisappearingMessagesConfiguration.DefaultDuration.disappearAfterRead.seconds,
|
|
type: .disappearAfterRead
|
|
)
|
|
)
|
|
}
|
|
}
|
|
),
|
|
SessionCell.Info(
|
|
id: "disappearingMessagesDisappearAfterSend".localized(),
|
|
title: "disappearingMessagesDisappearAfterSend".localized(),
|
|
subtitle: "disappearingMessagesDisappearAfterSendDescription".localized(),
|
|
trailingAccessory: .radio(
|
|
isSelected: (
|
|
currentConfig.isEnabled &&
|
|
currentConfig.type == .disappearAfterSend
|
|
),
|
|
accessibility: Accessibility(
|
|
identifier: "Disappear After Send - Radio"
|
|
)
|
|
),
|
|
accessibility: Accessibility(
|
|
identifier: "Disappear after send option",
|
|
label: "Disappear after send option"
|
|
),
|
|
onTap: { [weak self, originalConfig] in
|
|
switch (originalConfig.isEnabled, originalConfig.type) {
|
|
case (true, .disappearAfterSend): self?.configSubject.send(originalConfig)
|
|
default: self?.configSubject.send(
|
|
currentConfig.with(
|
|
isEnabled: true,
|
|
durationSeconds: DisappearingMessagesConfiguration.DefaultDuration.disappearAfterSend.seconds,
|
|
type: .disappearAfterSend
|
|
)
|
|
)
|
|
}
|
|
}
|
|
)
|
|
].compactMap { $0 }
|
|
),
|
|
(!currentConfig.isEnabled ? nil :
|
|
SectionModel(
|
|
model: (currentConfig.type == .disappearAfterSend ?
|
|
.timerDisappearAfterSend :
|
|
.timerDisappearAfterRead
|
|
),
|
|
elements: DisappearingMessagesConfiguration
|
|
.validDurationsSeconds(currentConfig.type ?? .disappearAfterSend, using: dependencies)
|
|
.map { duration in
|
|
let title: String = duration.formatted(format: .long)
|
|
|
|
return SessionCell.Info(
|
|
id: title,
|
|
title: title,
|
|
trailingAccessory: .radio(
|
|
isSelected: (
|
|
currentConfig.isEnabled &&
|
|
currentConfig.durationSeconds == duration
|
|
),
|
|
accessibility: Accessibility(
|
|
identifier: "\(title) - Radio"
|
|
)
|
|
),
|
|
accessibility: Accessibility(
|
|
identifier: "Time option",
|
|
label: "Time option"
|
|
),
|
|
onTap: { [weak self] in
|
|
self?.configSubject.send(
|
|
currentConfig.with(
|
|
durationSeconds: duration
|
|
)
|
|
)
|
|
}
|
|
)
|
|
}
|
|
)
|
|
)
|
|
].compactMap { $0 }
|
|
|
|
case (.legacyGroup, _), (.group, _), (_, true):
|
|
return [
|
|
SectionModel(
|
|
model: (isNoteToSelf ? .noteToSelf : .group),
|
|
elements: [
|
|
SessionCell.Info(
|
|
id: "off".localized(),
|
|
title: "off".localized(),
|
|
trailingAccessory: .radio(
|
|
isSelected: !currentConfig.isEnabled,
|
|
accessibility: Accessibility(
|
|
identifier: "Off - Radio"
|
|
)
|
|
),
|
|
isEnabled: (
|
|
isNoteToSelf ||
|
|
currentUserRole == .admin
|
|
),
|
|
accessibility: Accessibility(
|
|
identifier: "Disable disappearing messages (Off option)",
|
|
label: "Disable disappearing messages (Off option)"
|
|
),
|
|
onTap: { [weak self] in
|
|
self?.configSubject.send(
|
|
currentConfig.with(
|
|
isEnabled: false,
|
|
durationSeconds: DisappearingMessagesConfiguration.DefaultDuration.off.seconds
|
|
)
|
|
)
|
|
}
|
|
)
|
|
]
|
|
.compactMap { $0 }
|
|
.appending(
|
|
contentsOf: DisappearingMessagesConfiguration
|
|
.validDurationsSeconds(.disappearAfterSend, using: dependencies)
|
|
.map { duration in
|
|
let title: String = duration.formatted(format: .long)
|
|
|
|
return SessionCell.Info(
|
|
id: title,
|
|
title: title,
|
|
trailingAccessory: .radio(
|
|
isSelected: (
|
|
currentConfig.isEnabled &&
|
|
currentConfig.durationSeconds == duration
|
|
),
|
|
accessibility: Accessibility(
|
|
identifier: "\(title) - Radio"
|
|
)
|
|
),
|
|
isEnabled: (isNoteToSelf || currentUserRole == .admin),
|
|
accessibility: Accessibility(
|
|
identifier: "Time option",
|
|
label: "Time option"
|
|
),
|
|
onTap: { [weak self] in
|
|
self?.configSubject.send(
|
|
currentConfig.with(
|
|
isEnabled: true,
|
|
durationSeconds: duration,
|
|
type: .disappearAfterSend
|
|
)
|
|
)
|
|
}
|
|
)
|
|
}
|
|
)
|
|
)
|
|
].compactMap { $0 }
|
|
|
|
case (.community, _):
|
|
return [] // Should not happen
|
|
}
|
|
}
|
|
|
|
// MARK: - Functions
|
|
|
|
private func saveChanges() {
|
|
let updatedConfig: DisappearingMessagesConfiguration = self.configSubject.value
|
|
|
|
guard self.originalConfig != updatedConfig else { return }
|
|
|
|
// Custom handle updated groups first (all logic is consolidated in the MessageSender extension
|
|
switch threadVariant {
|
|
case .group:
|
|
MessageSender
|
|
.updateGroup(
|
|
groupSessionId: threadId,
|
|
disapperingMessagesConfig: updatedConfig,
|
|
using: dependencies
|
|
)
|
|
.sinkUntilComplete()
|
|
|
|
default: break
|
|
}
|
|
|
|
// Otherwise handle other conversation variants
|
|
dependencies[singleton: .storage].writeAsync { [threadId, threadVariant, dependencies] db in
|
|
// Update the local state
|
|
try updatedConfig.upserted(db)
|
|
|
|
let currentOffsetTimestampMs: UInt64 = dependencies[cache: .snodeAPI].currentOffsetTimestampMs()
|
|
let interactionId = try updatedConfig
|
|
.upserted(db)
|
|
.insertControlMessage(
|
|
db,
|
|
threadVariant: threadVariant,
|
|
authorId: dependencies[cache: .general].sessionId.hexString,
|
|
timestampMs: currentOffsetTimestampMs,
|
|
serverHash: nil,
|
|
serverExpirationTimestamp: nil,
|
|
using: dependencies
|
|
)?
|
|
.interactionId
|
|
|
|
// Update libSession
|
|
switch threadVariant {
|
|
case .contact:
|
|
try LibSession.update(
|
|
db,
|
|
sessionId: threadId,
|
|
disappearingMessagesConfig: updatedConfig,
|
|
using: dependencies
|
|
)
|
|
|
|
case .group: break // Handled above
|
|
default: break
|
|
}
|
|
|
|
// Send a control message that the disappearing messages setting changed
|
|
try MessageSender.send(
|
|
db,
|
|
message: ExpirationTimerUpdate()
|
|
.with(sentTimestampMs: UInt64(currentOffsetTimestampMs))
|
|
.with(updatedConfig),
|
|
interactionId: interactionId,
|
|
threadId: threadId,
|
|
threadVariant: threadVariant,
|
|
using: dependencies
|
|
)
|
|
|
|
/// Notify of update
|
|
db.addConversationEvent(
|
|
id: threadId,
|
|
variant: threadVariant,
|
|
type: .updated(.disappearingMessageConfiguration(updatedConfig))
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension String: @retroactive Differentiable {}
|