session-ios/Session/Settings/SettingsTableViewModel.swift
Morgan Pretty face9da02b Fixed search performance, started styling in-conversaiton settings
Fixed a bug where the scroll to bottom button wasn't working
Fixed an issue where searching was running on the main thread (which could cause UI issues)
Updated the searching to interrupt the previous query when the search term changes
Updated the in-conversation settings to be use the new config-based approach (deleted the OWSConversationSettingsViewController)
2022-09-07 17:37:01 +10:00

511 lines
17 KiB
Swift

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit.UIImage
import Combine
import GRDB
import DifferenceKit
import SessionUIKit
import SessionMessagingKit
import SessionUtilitiesKit
class SettingsTableViewModel<NavItemId: Equatable, Section: SettingSection, SettingItem: Hashable & Differentiable> {
typealias SectionModel = ArraySection<Section, SettingInfo<SettingItem>>
typealias ObservableData = ValueObservation<ValueReducers.RemoveDuplicates<ValueReducers.Fetch<[SectionModel]>>>
var closeNavItemId: NavItemId?
// MARK: - Initialization
/// Provide a `closeNavItemId` in order to show a close button
init(closeNavItemId: NavItemId? = nil) {
self.closeNavItemId = closeNavItemId
}
// MARK: - Input
let navItemTapped: PassthroughSubject<NavItemId, Never> = PassthroughSubject()
private let _isEditing: CurrentValueSubject<Bool, Never> = CurrentValueSubject(false)
lazy var isEditing: AnyPublisher<Bool, Never> = _isEditing
.removeDuplicates()
.shareReplay(1)
// MARK: - Navigation
open var leftNavItems: AnyPublisher<[NavItem]?, Never> {
guard let closeNavItemId: NavItemId = self.closeNavItemId else {
return Just(nil).eraseToAnyPublisher()
}
return Just([
NavItem(
id: closeNavItemId,
image: UIImage(named: "X")?
.withRenderingMode(.alwaysTemplate),
style: .plain,
accessibilityIdentifier: "Close Button"
)
]).eraseToAnyPublisher()
}
open var rightNavItems: AnyPublisher<[NavItem]?, Never> { Just(nil).eraseToAnyPublisher() }
open var closeScreen: AnyPublisher<Bool, Never> {
navItemTapped
.filter { [weak self] itemId in itemId == self?.closeNavItemId }
.map { _ in true }
.eraseToAnyPublisher()
}
// MARK: - Content
open var title: String { preconditionFailure("abstract class - override in subclass") }
open var settingsData: [SectionModel] { preconditionFailure("abstract class - override in subclass") }
open var observableSettingsData: ObservableData {
preconditionFailure("abstract class - override in subclass")
}
func updateSettings(_ updatedSettings: [SectionModel]) {
preconditionFailure("abstract class - override in subclass")
}
func setIsEditing(_ isEditing: Bool) {
_isEditing.send(isEditing)
}
}
// MARK: - NavItem
public enum NoNav: Equatable {}
extension SettingsTableViewModel {
public struct NavItem {
let id: NavItemId
let image: UIImage?
let style: UIBarButtonItem.Style
let systemItem: UIBarButtonItem.SystemItem?
let accessibilityIdentifier: String
// MARK: - Initialization
public init(
id: NavItemId,
systemItem: UIBarButtonItem.SystemItem?,
accessibilityIdentifier: String
) {
self.id = id
self.image = nil
self.style = .plain
self.systemItem = systemItem
self.accessibilityIdentifier = accessibilityIdentifier
}
public init(
id: NavItemId,
image: UIImage?,
style: UIBarButtonItem.Style,
accessibilityIdentifier: String
) {
self.id = id
self.image = image
self.style = style
self.systemItem = nil
self.accessibilityIdentifier = accessibilityIdentifier
}
// MARK: - Functions
public func createBarButtonItem() -> DisposableBarButtonItem {
guard let systemItem: UIBarButtonItem.SystemItem = systemItem else {
return DisposableBarButtonItem(
image: image,
style: style,
target: nil,
action: nil,
accessibilityIdentifier: accessibilityIdentifier
)
}
return DisposableBarButtonItem(
barButtonSystemItem: systemItem,
target: nil,
action: nil,
accessibilityIdentifier: accessibilityIdentifier
)
}
}
}
// MARK: - SettingSectionHeaderStyle
public enum SettingSectionHeaderStyle: Differentiable {
case none
case title
case padding
}
// MARK: - SettingSection
protocol SettingSection: Differentiable {
var title: String? { get }
var style: SettingSectionHeaderStyle { get }
}
extension SettingSection {
var title: String? { nil }
var style: SettingSectionHeaderStyle { .none }
}
// MARK: - SettingInfo
struct SettingInfo<ID: Hashable & Differentiable>: Equatable, Hashable, Differentiable {
let id: ID
let icon: UIImage?
let title: String
let subtitle: String?
let alignment: NSTextAlignment
let accessibilityIdentifier: String?
let action: SettingsAction
let subtitleExtraViewGenerator: (() -> UIView)?
let extraActionTitle: ((Theme, Theme.PrimaryColor) -> NSAttributedString)?
let onExtraAction: (() -> Void)?
// MARK: - Initialization
init(
id: ID,
icon: UIImage? = nil,
title: String,
subtitle: String? = nil,
alignment: NSTextAlignment = .left,
accessibilityIdentifier: String? = nil,
subtitleExtraViewGenerator: (() -> UIView)? = nil,
action: SettingsAction,
extraActionTitle: ((Theme, Theme.PrimaryColor) -> NSAttributedString)? = nil,
onExtraAction: (() -> Void)? = nil
) {
self.id = id
self.icon = icon
self.title = title
self.subtitle = subtitle
self.alignment = alignment
self.accessibilityIdentifier = accessibilityIdentifier
self.subtitleExtraViewGenerator = subtitleExtraViewGenerator
self.action = action
self.extraActionTitle = extraActionTitle
self.onExtraAction = onExtraAction
}
// MARK: - Conformance
var differenceIdentifier: ID { id }
func hash(into hasher: inout Hasher) {
id.hash(into: &hasher)
icon.hash(into: &hasher)
title.hash(into: &hasher)
subtitle.hash(into: &hasher)
alignment.hash(into: &hasher)
accessibilityIdentifier.hash(into: &hasher)
action.hash(into: &hasher)
}
static func == (lhs: SettingInfo<ID>, rhs: SettingInfo<ID>) -> Bool {
return (
lhs.id == rhs.id &&
lhs.icon == rhs.icon &&
lhs.title == rhs.title &&
lhs.subtitle == rhs.subtitle &&
lhs.alignment == rhs.alignment &&
lhs.accessibilityIdentifier == rhs.accessibilityIdentifier &&
lhs.action == rhs.action
)
}
// MARK: - Mutation
func with(action: SettingsAction) -> SettingInfo {
return SettingInfo(
id: self.id,
icon: self.icon,
title: self.title,
subtitle: self.subtitle,
alignment: self.alignment,
accessibilityIdentifier: self.accessibilityIdentifier,
subtitleExtraViewGenerator: self.subtitleExtraViewGenerator,
action: action,
extraActionTitle: self.extraActionTitle,
onExtraAction: self.onExtraAction
)
}
}
// MARK: - SettingsAction
public enum SettingsAction: Hashable, Equatable {
case threadInfo(
threadViewModel: SessionThreadViewModel,
style: ThreadInfoStyle = ThreadInfoStyle(),
createAvatarTapDestination: (() -> UIViewController?)? = nil,
titleTapped: (() -> Void)? = nil,
titleChanged: ((String) -> Void)? = nil
)
case userDefaultsBool(
defaults: UserDefaults,
key: String,
isEnabled: Bool = true,
onChange: (() -> Void)?
)
case settingBool(
key: Setting.BoolKey,
confirmationInfo: ConfirmationModal.Info?,
isEnabled: Bool = true
)
case customToggle(
value: Bool,
isEnabled: Bool = true,
confirmationInfo: ConfirmationModal.Info? = nil,
onChange: ((Bool) -> Void)? = nil
)
case settingEnum(
key: String,
title: String?,
createUpdateScreen: () -> UIViewController
)
case generalEnum(
title: String?,
createUpdateScreen: () -> UIViewController
)
case trigger(
showChevron: Bool = true,
action: () -> Void
)
case push(
showChevron: Bool = true,
textColor: ThemeValue = .textPrimary,
shouldHaveBackground: Bool = true,
createDestination: () -> UIViewController
)
case present(createDestination: () -> UIViewController)
case listSelection(
isSelected: () -> Bool,
storedSelection: Bool,
shouldAutoSave: Bool,
selectValue: () -> Void
)
case rightButtonAction(
title: String,
action: (UIView) -> ()
)
private var actionName: String {
switch self {
case .threadInfo: return "threadInfo"
case .userDefaultsBool: return "userDefaultsBool"
case .settingBool: return "settingBool"
case .customToggle: return "customToggle"
case .settingEnum: return "settingEnum"
case .generalEnum: return "generalEnum"
case .trigger: return "trigger"
case .push: return "push"
case .present: return "present"
case .listSelection: return "listSelection"
case .rightButtonAction: return "rightButtonAction"
}
}
var shouldHaveBackground: Bool {
switch self {
case .threadInfo: return false
case .push(_, _, let shouldHaveBackground, _): return shouldHaveBackground
default: return true
}
}
// MARK: - Convenience
public static func settingEnum<ET: EnumIntSetting>(
_ db: Database,
type: ET.Type,
key: Setting.EnumKey,
titleGenerator: @escaping ((ET?) -> String?),
createUpdateScreen: @escaping () -> UIViewController
) -> SettingsAction {
return SettingsAction.settingEnum(
key: key.rawValue,
title: titleGenerator(db[key]),
createUpdateScreen: createUpdateScreen
)
}
public static func settingEnum<ET: EnumStringSetting>(
_ db: Database,
type: ET.Type,
key: Setting.EnumKey,
titleGenerator: @escaping ((ET?) -> String?),
createUpdateScreen: @escaping () -> UIViewController
) -> SettingsAction {
return SettingsAction.settingEnum(
key: key.rawValue,
title: titleGenerator(db[key]),
createUpdateScreen: createUpdateScreen
)
}
public static func settingBool(key: Setting.BoolKey) -> SettingsAction {
return .settingBool(key: key, confirmationInfo: nil)
}
// MARK: - Conformance
public func hash(into hasher: inout Hasher) {
actionName.hash(into: &hasher)
switch self {
case .threadInfo(let threadViewModel, let style, _, _, _):
threadViewModel.hash(into: &hasher)
style.hash(into: &hasher)
case .userDefaultsBool(_, let key, let isEnabled, _):
key.hash(into: &hasher)
isEnabled.hash(into: &hasher)
case .settingBool(let key, let confirmationInfo, let isEnabled):
key.hash(into: &hasher)
confirmationInfo.hash(into: &hasher)
isEnabled.hash(into: &hasher)
case .customToggle(let value, let isEnabled, let confirmationInfo, _):
value.hash(into: &hasher)
isEnabled.hash(into: &hasher)
confirmationInfo.hash(into: &hasher)
case .settingEnum(let key, let title, _):
key.hash(into: &hasher)
title.hash(into: &hasher)
case .generalEnum(let title, _):
title.hash(into: &hasher)
case .trigger(let showChevron, _):
showChevron.hash(into: &hasher)
case .push(let showChevron, let textColor, let shouldHaveBackground, _):
showChevron.hash(into: &hasher)
textColor.hash(into: &hasher)
shouldHaveBackground.hash(into: &hasher)
case .present(_): break
case .listSelection(let isSelected, let storedSelection, let shouldAutoSave, _):
isSelected().hash(into: &hasher)
storedSelection.hash(into: &hasher)
shouldAutoSave.hash(into: &hasher)
case .rightButtonAction(let title, _):
title.hash(into: &hasher)
}
}
public static func == (lhs: SettingsAction, rhs: SettingsAction) -> Bool {
switch (lhs, rhs) {
case (.threadInfo(let lhsThreadViewModel, let lhsStyle, _, _, _), .threadInfo(let rhsThreadViewModel, let rhsStyle, _, _, _)):
return (
lhsThreadViewModel == rhsThreadViewModel &&
lhsStyle == rhsStyle
)
case (.userDefaultsBool(_, let lhsKey, let lhsIsEnabled, _), .userDefaultsBool(_, let rhsKey, let rhsIsEnabled, _)):
return (
lhsKey == rhsKey &&
lhsIsEnabled == rhsIsEnabled
)
case (.settingBool(let lhsKey, let lhsConfirmationInfo, let lhsIsEnabled), .settingBool(let rhsKey, let rhsConfirmationInfo, let rhsIsEnabled)):
return (
lhsKey == rhsKey &&
lhsConfirmationInfo == rhsConfirmationInfo &&
lhsIsEnabled == rhsIsEnabled
)
case (.customToggle(let lhsValue, let lhsIsEnabled, let lhsConfirmationInfo, _), .customToggle(let rhsValue, let rhsIsEnabled, let rhsConfirmationInfo, _)):
return (
lhsValue == rhsValue &&
lhsIsEnabled == rhsIsEnabled &&
lhsConfirmationInfo == rhsConfirmationInfo
)
case (.settingEnum(let lhsKey, let lhsTitle, _), .settingEnum(let rhsKey, let rhsTitle, _)):
return (
lhsKey == rhsKey &&
lhsTitle == rhsTitle
)
case (.generalEnum(let lhsTitle, _), .generalEnum(let rhsTitle, _)):
return (lhsTitle == rhsTitle)
case (.trigger(let lhsShowChevron, _), .trigger(let rhsShowChevron, _)):
return (lhsShowChevron == rhsShowChevron)
case (.push(let lhsShowChevron, let lhsTextColor, let lhsHasBackground, _), .push(let rhsShowChevron, let rhsTextColor, let rhsHasBackground, _)):
return (
lhsShowChevron == rhsShowChevron &&
lhsTextColor == rhsTextColor &&
lhsHasBackground == rhsHasBackground
)
case (.present(_), .present(_)): return true
case (.listSelection(let lhsIsSelected, let lhsStoredSelection, let lhsShouldAutoSave, _), .listSelection(let rhsIsSelected, let rhsStoredSelection, let rhsShouldAutoSave, _)):
return (
lhsIsSelected() == rhsIsSelected() &&
lhsStoredSelection == rhsStoredSelection &&
lhsShouldAutoSave == rhsShouldAutoSave
)
case (.rightButtonAction(let lhsTitle, _), .rightButtonAction(let rhsTitle, _)):
return (lhsTitle == rhsTitle)
default: return false
}
}
}
// MARK: - ThreadInfoStyle
public struct ThreadInfoStyle: Hashable, Equatable {
public enum Style: Hashable, Equatable {
case small
case monoSmall
case monoLarge
}
public struct Action: Hashable, Equatable {
let title: String
let run: () -> ()
public func hash(into hasher: inout Hasher) {
title.hash(into: &hasher)
}
public static func == (lhs: Action, rhs: Action) -> Bool {
return (lhs.title == rhs.title)
}
}
public let separatorTitle: String?
public let descriptionStyle: Style
public let descriptionActions: [Action]
public init(
separatorTitle: String? = nil,
descriptionStyle: Style = .monoSmall,
descriptionActions: [Action] = []
) {
self.separatorTitle = separatorTitle
self.descriptionStyle = descriptionStyle
self.descriptionActions = descriptionActions
}
}