mirror of https://github.com/oxen-io/session-ios
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
629 lines
31 KiB
Swift
629 lines
31 KiB
Swift
3 years ago
|
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||
|
|
||
|
import Foundation
|
||
|
import Combine
|
||
|
import GRDB
|
||
|
import DifferenceKit
|
||
|
import SessionUIKit
|
||
|
import SessionMessagingKit
|
||
|
import SignalUtilitiesKit
|
||
|
import SessionUtilitiesKit
|
||
|
|
||
|
class ThreadSettingsViewModel: SettingsTableViewModel<ThreadSettingsViewModel.NavButton, ThreadSettingsViewModel.Section, ThreadSettingsViewModel.Setting> {
|
||
|
// MARK: - Config
|
||
|
|
||
|
enum NavState {
|
||
|
case standard
|
||
|
case editing
|
||
|
}
|
||
|
|
||
|
enum NavButton: Equatable {
|
||
|
case edit
|
||
|
case cancel
|
||
|
case done
|
||
|
}
|
||
|
|
||
|
public enum Section: SettingSection {
|
||
|
case content
|
||
|
}
|
||
|
|
||
|
public enum Setting: Differentiable {
|
||
|
case threadInfo
|
||
|
case copyThreadId
|
||
|
case allMedia
|
||
|
case searchConversation
|
||
|
case addToOpenGroup
|
||
|
case disappearingMessages
|
||
|
case disappearingMessagesDuration
|
||
|
case editGroup
|
||
|
case leaveGroup
|
||
|
case notificationSound
|
||
|
case notificationMentionsOnly
|
||
|
case notificationMute
|
||
|
case blockUser
|
||
|
}
|
||
|
|
||
|
// MARK: - Variables
|
||
|
|
||
|
private let threadId: String
|
||
|
private let threadVariant: SessionThread.Variant
|
||
|
private let didTriggerSearch: () -> ()
|
||
|
private var oldDisplayName: String?
|
||
|
private var editedDisplayName: String?
|
||
|
|
||
|
// MARK: - Initialization
|
||
|
|
||
|
init(threadId: String, threadVariant: SessionThread.Variant, didTriggerSearch: @escaping () -> ()) {
|
||
|
self.threadId = threadId
|
||
|
self.threadVariant = threadVariant
|
||
|
self.didTriggerSearch = didTriggerSearch
|
||
|
self.oldDisplayName = (threadVariant != .contact ?
|
||
|
nil :
|
||
|
Storage.shared.read { db in
|
||
|
try Profile
|
||
|
.filter(id: threadId)
|
||
|
.select(.nickname)
|
||
|
.asRequest(of: String.self)
|
||
|
.fetchOne(db)
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
|
||
|
// MARK: - Navigation
|
||
|
|
||
|
lazy var navState: AnyPublisher<NavState, Never> = {
|
||
|
Publishers
|
||
|
.MergeMany(
|
||
|
isEditing
|
||
|
.filter { $0 }
|
||
|
.map { _ in .editing }
|
||
|
.eraseToAnyPublisher(),
|
||
|
navItemTapped
|
||
|
.filter { $0 == .edit }
|
||
|
.map { _ in .editing }
|
||
|
.handleEvents(receiveOutput: { [weak self] _ in
|
||
|
self?.setIsEditing(true)
|
||
|
})
|
||
|
.eraseToAnyPublisher(),
|
||
|
navItemTapped
|
||
|
.filter { $0 == .cancel }
|
||
|
.map { _ in .standard }
|
||
|
.handleEvents(receiveOutput: { [weak self] _ in
|
||
|
self?.setIsEditing(false)
|
||
|
self?.editedDisplayName = self?.oldDisplayName
|
||
|
})
|
||
|
.eraseToAnyPublisher(),
|
||
|
navItemTapped
|
||
|
.filter { $0 == .done }
|
||
|
.filter { [weak self] _ in self?.threadVariant == .contact }
|
||
|
.handleEvents(receiveOutput: { [weak self] _ in
|
||
|
self?.setIsEditing(false)
|
||
|
|
||
|
guard
|
||
|
let threadId: String = self?.threadId,
|
||
|
let editedDisplayName: String = self?.editedDisplayName
|
||
|
else { return }
|
||
|
|
||
|
let updatedNickname: String = editedDisplayName
|
||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
|
self?.oldDisplayName = (updatedNickname.isEmpty ? nil : editedDisplayName)
|
||
|
|
||
|
Storage.shared.writeAsync { db in
|
||
|
try Profile
|
||
|
.filter(id: threadId)
|
||
|
.updateAll(
|
||
|
db,
|
||
|
Profile.Columns.nickname
|
||
|
.set(to: (updatedNickname.isEmpty ? nil : editedDisplayName))
|
||
|
)
|
||
|
}
|
||
|
})
|
||
|
.map { _ in .standard }
|
||
|
.eraseToAnyPublisher()
|
||
|
)
|
||
|
.removeDuplicates()
|
||
|
.prepend(.standard) // Initial value
|
||
|
.eraseToAnyPublisher()
|
||
|
}()
|
||
|
|
||
|
override var leftNavItems: AnyPublisher<[NavItem]?, Never> {
|
||
|
navState
|
||
|
.map { [weak self] navState -> [NavItem] in
|
||
|
// Only show the 'Edit' button if it's a contact thread
|
||
|
guard self?.threadVariant == .contact else { return [] }
|
||
|
guard navState == .editing else { return [] }
|
||
|
|
||
|
return [
|
||
|
NavItem(
|
||
|
id: .cancel,
|
||
|
systemItem: .cancel,
|
||
|
accessibilityIdentifier: "Cancel button"
|
||
|
)
|
||
|
]
|
||
|
}
|
||
|
.eraseToAnyPublisher()
|
||
|
}
|
||
|
|
||
|
override var rightNavItems: AnyPublisher<[NavItem]?, Never> {
|
||
|
navState
|
||
|
.map { [weak self] navState -> [NavItem] in
|
||
|
// Only show the 'Edit' button if it's a contact thread
|
||
|
guard self?.threadVariant == .contact else { return [] }
|
||
|
|
||
|
switch navState {
|
||
|
case .editing:
|
||
|
return [
|
||
|
NavItem(
|
||
|
id: .done,
|
||
|
systemItem: .done,
|
||
|
accessibilityIdentifier: "Done button"
|
||
|
)
|
||
|
]
|
||
|
|
||
|
case .standard:
|
||
|
return [
|
||
|
NavItem(
|
||
|
id: .edit,
|
||
|
systemItem: .edit,
|
||
|
accessibilityIdentifier: "Edit button"
|
||
|
)
|
||
|
]
|
||
|
}
|
||
|
}
|
||
|
.eraseToAnyPublisher()
|
||
|
}
|
||
|
|
||
|
// MARK: - Content
|
||
|
|
||
|
override var title: String {
|
||
|
switch threadVariant {
|
||
|
case .contact: return "vc_settings_title".localized()
|
||
|
case .closedGroup, .openGroup: return "vc_group_settings_title".localized()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private var _settingsData: [SectionModel] = []
|
||
|
public override var settingsData: [SectionModel] { _settingsData }
|
||
|
|
||
|
public override var observableSettingsData: ObservableData { _observableSettingsData }
|
||
|
|
||
|
/// This is all the data the screen needs to populate itself, please see the following link for tips to help optimise
|
||
|
/// performance https://github.com/groue/GRDB.swift#valueobservation-performance
|
||
|
///
|
||
|
/// **Note:** This observation will be triggered twice immediately (and be de-duped by the `removeDuplicates`)
|
||
|
/// this is due to the behaviour of `ValueConcurrentObserver.asyncStartObservation` which triggers it's own
|
||
|
/// fetch (after the ones in `ValueConcurrentObserver.asyncStart`/`ValueConcurrentObserver.syncStart`)
|
||
|
/// just in case the database has changed between the two reads - unfortunately it doesn't look like there is a way to prevent this
|
||
|
private lazy var _observableSettingsData: ObservableData = ValueObservation
|
||
|
.trackingConstantRegion { [weak self, threadId = self.threadId, threadVariant = self.threadVariant] db -> [SectionModel] in
|
||
|
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||
|
let maybeThreadViewModel: SessionThreadViewModel? = try SessionThreadViewModel
|
||
|
.conversationSettingsQuery(threadId: threadId, userPublicKey: userPublicKey)
|
||
|
.fetchOne(db)
|
||
|
|
||
|
guard let threadViewModel: SessionThreadViewModel = maybeThreadViewModel else { return [] }
|
||
|
|
||
|
// Additional Queries
|
||
|
let fallbackSound: Preferences.Sound = db[.defaultNotificationSound]
|
||
|
.defaulting(to: Preferences.Sound.defaultNotificationSound)
|
||
|
let notificationSound: Preferences.Sound = try SessionThread
|
||
|
.filter(id: threadId)
|
||
|
.select(.notificationSound)
|
||
|
.asRequest(of: Preferences.Sound.self)
|
||
|
.fetchOne(db)
|
||
|
.defaulting(to: fallbackSound)
|
||
|
let disappearingMessagesConfig: DisappearingMessagesConfiguration = try DisappearingMessagesConfiguration
|
||
|
.fetchOne(db, id: threadId)
|
||
|
.defaulting(to: DisappearingMessagesConfiguration.defaultWith(threadId))
|
||
|
let currentUserIsClosedGroupMember: Bool = (
|
||
|
threadVariant == .closedGroup &&
|
||
|
threadViewModel.currentUserIsClosedGroupMember == true
|
||
|
)
|
||
|
|
||
|
return [
|
||
|
SectionModel(
|
||
|
model: .content,
|
||
|
elements: [
|
||
|
SettingInfo(
|
||
|
id: .threadInfo,
|
||
|
title: threadViewModel.displayName,
|
||
|
action: .threadInfo(
|
||
|
threadViewModel: threadViewModel,
|
||
|
createAvatarTapDestination: { [weak self] in
|
||
|
guard
|
||
|
threadVariant == .contact,
|
||
|
let profileData: Data = ProfileManager.profileAvatar(id: threadId)
|
||
|
else { return nil }
|
||
|
|
||
|
let format: ImageFormat = profileData.guessedImageFormat
|
||
|
let navController: UINavigationController = UINavigationController(
|
||
|
rootViewController: ProfilePictureVC(
|
||
|
image: (format == .gif || format == .webp ?
|
||
|
nil :
|
||
|
UIImage(data: profileData)
|
||
|
),
|
||
|
animatedImage: (format != .gif && format != .webp ?
|
||
|
nil :
|
||
|
YYImage(data: profileData)
|
||
|
),
|
||
|
title: threadViewModel.displayName
|
||
|
)
|
||
|
)
|
||
|
navController.modalPresentationStyle = .fullScreen
|
||
|
|
||
|
return navController
|
||
|
},
|
||
|
titleTapped: { [weak self] in self?.setIsEditing(true) },
|
||
|
titleChanged: { [weak self] text in self?.editedDisplayName = text }
|
||
|
)
|
||
|
),
|
||
|
|
||
|
(threadVariant == .closedGroup ? nil :
|
||
|
SettingInfo(
|
||
|
id: .copyThreadId,
|
||
|
icon: UIImage(named: "ic_copy")?
|
||
|
.withRenderingMode(.alwaysTemplate),
|
||
|
title: (threadVariant == .openGroup ?
|
||
|
"COPY_GROUP_URL".localized() :
|
||
|
"vc_conversation_settings_copy_session_id_button_title".localized()
|
||
|
),
|
||
|
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).copy_thread_id",
|
||
|
action: .trigger(showChevron: false) {
|
||
|
UIPasteboard.general.string = threadId
|
||
|
}
|
||
|
)
|
||
|
),
|
||
|
|
||
|
SettingInfo(
|
||
|
id: .allMedia,
|
||
|
icon: UIImage(named: "actionsheet_camera_roll_black")?
|
||
|
.withRenderingMode(.alwaysTemplate),
|
||
|
title: MediaStrings.allMedia,
|
||
|
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).all_media",
|
||
|
action: .push(showChevron: false) {
|
||
|
return MediaGalleryViewModel.createTileViewController(
|
||
|
threadId: threadId,
|
||
|
threadVariant: threadVariant,
|
||
|
focusedAttachmentId: nil
|
||
|
)
|
||
|
}
|
||
|
),
|
||
|
|
||
|
SettingInfo(
|
||
|
id: .searchConversation,
|
||
|
icon: UIImage(named: "conversation_settings_search")?
|
||
|
.withRenderingMode(.alwaysTemplate),
|
||
|
title: "CONVERSATION_SETTINGS_SEARCH".localized(),
|
||
|
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).search",
|
||
|
action: .trigger(showChevron: false) { [weak self] in
|
||
|
self?.didTriggerSearch()
|
||
|
}
|
||
|
),
|
||
|
|
||
|
(threadVariant != .openGroup ? nil :
|
||
|
SettingInfo(
|
||
|
id: .addToOpenGroup,
|
||
|
icon: UIImage(named: "ic_plus_24")?
|
||
|
.withRenderingMode(.alwaysTemplate),
|
||
|
title: "vc_conversation_settings_invite_button_title".localized(),
|
||
|
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).add_to_open_group",
|
||
|
action: .push(showChevron: false) {
|
||
|
return UserSelectionVC(
|
||
|
with: "vc_conversation_settings_invite_button_title".localized(),
|
||
|
excluding: Set()
|
||
|
) { [weak self] selectedUsers in
|
||
|
self?.addUsersToOpenGoup(selectedUsers: selectedUsers)
|
||
|
}
|
||
|
}
|
||
|
)
|
||
|
),
|
||
|
|
||
|
(threadVariant == .openGroup || threadViewModel.threadIsBlocked == true ? nil :
|
||
|
SettingInfo(
|
||
|
id: .disappearingMessages,
|
||
|
icon: UIImage(
|
||
|
named: (disappearingMessagesConfig.isEnabled ?
|
||
|
"ic_timer" :
|
||
|
"ic_timer_disabled"
|
||
|
)
|
||
|
)?.withRenderingMode(.alwaysTemplate),
|
||
|
title: "DISAPPEARING_MESSAGES".localized(),
|
||
|
subtitle: {
|
||
|
guard threadId != userPublicKey else {
|
||
|
return "When enabled, messages will disappear after they have been seen."
|
||
|
}
|
||
|
|
||
|
let customDisplayName: String = {
|
||
|
switch threadVariant {
|
||
|
case .closedGroup, .openGroup: return "the group"
|
||
|
case .contact: return threadViewModel.displayName
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
return String(
|
||
|
format: "When enabled, messages between you and %@ will disappear after they have been seen.",
|
||
|
arguments: [customDisplayName]
|
||
|
)
|
||
|
}(),
|
||
|
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).disappearing_messages",
|
||
|
action: .generalEnum(
|
||
|
title: (disappearingMessagesConfig.isEnabled ?
|
||
|
disappearingMessagesConfig.durationString :
|
||
|
"DISAPPEARING_MESSAGES_OFF".localized()
|
||
|
),
|
||
|
createUpdateScreen: {
|
||
|
SettingsTableViewController(
|
||
|
viewModel: ThreadDisappearingMessagesViewModel(
|
||
|
threadId: threadId,
|
||
|
config: disappearingMessagesConfig
|
||
|
)
|
||
|
)
|
||
|
}
|
||
|
)
|
||
|
)
|
||
|
),
|
||
|
|
||
|
(!currentUserIsClosedGroupMember ? nil :
|
||
|
SettingInfo(
|
||
|
id: .editGroup,
|
||
|
icon: UIImage(named: "table_ic_group_edit")?
|
||
|
.withRenderingMode(.alwaysTemplate),
|
||
|
title: "EDIT_GROUP_ACTION".localized(),
|
||
|
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).edit_group",
|
||
|
action: .push(showChevron: false) {
|
||
|
EditClosedGroupVC(threadId: threadId)
|
||
|
}
|
||
|
)
|
||
|
),
|
||
|
|
||
|
(!currentUserIsClosedGroupMember ? nil :
|
||
|
SettingInfo(
|
||
|
id: .leaveGroup,
|
||
|
icon: UIImage(named: "table_ic_group_leave")?
|
||
|
.withRenderingMode(.alwaysTemplate),
|
||
|
title: "LEAVE_GROUP_ACTION".localized(),
|
||
|
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).leave_group",
|
||
|
action: .present {
|
||
|
ConfirmationModal(
|
||
|
info: ConfirmationModal.Info(
|
||
|
title: "CONFIRM_LEAVE_GROUP_TITLE".localized(),
|
||
|
explanation: (currentUserIsClosedGroupMember ?
|
||
|
"Because you are the creator of this group it will be deleted for everyone. This cannot be undone." :
|
||
|
"CONFIRM_LEAVE_GROUP_DESCRIPTION".localized()
|
||
|
),
|
||
|
confirmTitle: "LEAVE_BUTTON_TITLE".localized(),
|
||
|
confirmStyle: .danger,
|
||
|
cancelStyle: .textPrimary
|
||
|
) { _ in
|
||
|
Storage.shared.writeAsync { db in
|
||
|
try MessageSender.leave(db, groupPublicKey: threadId)
|
||
|
}
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
)
|
||
|
),
|
||
|
|
||
|
(threadViewModel.threadIsNoteToSelf ? nil :
|
||
|
SettingInfo(
|
||
|
id: .notificationSound,
|
||
|
icon: UIImage(named: "table_ic_notification_sound")?
|
||
|
.withRenderingMode(.alwaysTemplate),
|
||
|
title: "SETTINGS_ITEM_NOTIFICATION_SOUND".localized(),
|
||
|
action: .generalEnum(
|
||
|
title: notificationSound.displayName,
|
||
|
createUpdateScreen: {
|
||
|
SettingsTableViewController(
|
||
|
viewModel: NotificationSoundViewModel(threadId: threadId)
|
||
|
)
|
||
|
}
|
||
|
)
|
||
|
)
|
||
|
),
|
||
|
|
||
|
(threadVariant == .contact ? nil :
|
||
|
SettingInfo(
|
||
|
id: .notificationMentionsOnly,
|
||
|
icon: UIImage(named: "NotifyMentions")?
|
||
|
.withRenderingMode(.alwaysTemplate),
|
||
|
title: "vc_conversation_settings_notify_for_mentions_only_title".localized(),
|
||
|
subtitle: "vc_conversation_settings_notify_for_mentions_only_explanation".localized(),
|
||
|
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).notify_for_mentions_only",
|
||
|
action: .customToggle(
|
||
|
value: (threadViewModel.threadOnlyNotifyForMentions == true),
|
||
|
isEnabled: (
|
||
|
threadViewModel.threadVariant != .closedGroup ||
|
||
|
currentUserIsClosedGroupMember
|
||
|
)
|
||
|
) { newValue in
|
||
|
Storage.shared.writeAsync { db in
|
||
|
try SessionThread
|
||
|
.filter(id: threadId)
|
||
|
.updateAll(
|
||
|
db,
|
||
|
SessionThread.Columns.onlyNotifyForMentions.set(to: newValue)
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
)
|
||
|
),
|
||
|
|
||
|
(threadViewModel.threadIsNoteToSelf ? nil :
|
||
|
SettingInfo(
|
||
|
id: .notificationMute,
|
||
|
icon: UIImage(named: "Mute")?
|
||
|
.withRenderingMode(.alwaysTemplate),
|
||
|
title: "CONVERSATION_SETTINGS_MUTE_LABEL".localized(),
|
||
|
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).mute",
|
||
|
action: .customToggle(
|
||
|
value: (threadViewModel.threadMutedUntilTimestamp != nil),
|
||
|
isEnabled: (
|
||
|
threadViewModel.threadVariant != .closedGroup ||
|
||
|
currentUserIsClosedGroupMember
|
||
|
)
|
||
|
) { newValue in
|
||
|
Storage.shared.writeAsync { db in
|
||
|
try SessionThread
|
||
|
.filter(id: threadId)
|
||
|
.updateAll(
|
||
|
db,
|
||
|
SessionThread.Columns.mutedUntilTimestamp.set(
|
||
|
to: (newValue ?
|
||
|
Date.distantFuture.timeIntervalSince1970 :
|
||
|
nil
|
||
|
)
|
||
|
)
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
)
|
||
|
),
|
||
|
|
||
|
(threadViewModel.threadIsNoteToSelf || threadVariant != .contact ? nil :
|
||
|
SettingInfo(
|
||
|
id: .blockUser,
|
||
|
icon: UIImage(named: "table_ic_block")?
|
||
|
.withRenderingMode(.alwaysTemplate),
|
||
|
title: "CONVERSATION_SETTINGS_BLOCK_THIS_USER".localized(),
|
||
|
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).block",
|
||
|
action: .customToggle(
|
||
|
value: (threadViewModel.threadIsBlocked == true),
|
||
|
confirmationInfo: ConfirmationModal.Info(
|
||
|
title: {
|
||
|
guard threadViewModel.threadIsBlocked == true else {
|
||
|
return String(
|
||
|
format: "BLOCK_LIST_BLOCK_USER_TITLE_FORMAT".localized(),
|
||
|
threadViewModel.displayName
|
||
|
)
|
||
|
}
|
||
|
|
||
|
return String(
|
||
|
format: "BLOCK_LIST_UNBLOCK_TITLE_FORMAT".localized(),
|
||
|
threadViewModel.displayName
|
||
|
)
|
||
|
}(),
|
||
|
explanation: (threadViewModel.threadIsBlocked == true ?
|
||
|
nil :
|
||
|
"BLOCK_USER_BEHAVIOR_EXPLANATION".localized()
|
||
|
),
|
||
|
confirmTitle: (threadViewModel.threadIsBlocked == true ?
|
||
|
"BLOCK_LIST_UNBLOCK_BUTTON".localized() :
|
||
|
"BLOCK_LIST_BLOCK_BUTTON".localized()
|
||
|
),
|
||
|
confirmStyle: .danger,
|
||
|
cancelStyle: .textPrimary
|
||
|
) { viewController in
|
||
|
let isBlocked: Bool = (threadViewModel.threadIsBlocked == true)
|
||
|
|
||
|
self?.updateBlockedState(
|
||
|
from: isBlocked,
|
||
|
isBlocked: !isBlocked,
|
||
|
threadId: threadId,
|
||
|
displayName: threadViewModel.displayName,
|
||
|
viewController: viewController
|
||
|
)
|
||
|
}
|
||
|
)
|
||
|
)
|
||
|
)
|
||
|
].compactMap { $0 }
|
||
|
)
|
||
|
]
|
||
|
}
|
||
|
.removeDuplicates()
|
||
|
|
||
|
// MARK: - Functions
|
||
|
|
||
|
public override func updateSettings(_ updatedSettings: [SectionModel]) {
|
||
|
self._settingsData = updatedSettings
|
||
|
}
|
||
|
|
||
|
private func addUsersToOpenGoup(selectedUsers: Set<String>) {
|
||
|
let threadId: String = self.threadId
|
||
|
|
||
|
Storage.shared.writeAsync { db in
|
||
|
guard let openGroup: OpenGroup = try OpenGroup.fetchOne(db, id: threadId) else { return }
|
||
|
|
||
|
let urlString: String = "\(openGroup.server)/\(openGroup.roomToken)?public_key=\(openGroup.publicKey)"
|
||
|
|
||
|
try selectedUsers.forEach { userId in
|
||
|
let thread: SessionThread = try SessionThread.fetchOrCreate(db, id: userId, variant: .contact)
|
||
|
|
||
|
try LinkPreview(
|
||
|
url: urlString,
|
||
|
variant: .openGroupInvitation,
|
||
|
title: openGroup.name
|
||
|
)
|
||
|
.save(db)
|
||
|
|
||
|
let interaction: Interaction = try Interaction(
|
||
|
threadId: thread.id,
|
||
|
authorId: userId,
|
||
|
variant: .standardOutgoing,
|
||
|
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)),
|
||
|
expiresInSeconds: try? DisappearingMessagesConfiguration
|
||
|
.select(.durationSeconds)
|
||
|
.filter(id: userId)
|
||
|
.filter(DisappearingMessagesConfiguration.Columns.isEnabled == true)
|
||
|
.asRequest(of: TimeInterval.self)
|
||
|
.fetchOne(db),
|
||
|
linkPreviewUrl: urlString
|
||
|
)
|
||
|
.inserted(db)
|
||
|
|
||
|
try MessageSender.send(
|
||
|
db,
|
||
|
interaction: interaction,
|
||
|
in: thread
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private func updateBlockedState(
|
||
|
from oldBlockedState: Bool,
|
||
|
isBlocked: Bool,
|
||
|
threadId: String,
|
||
|
displayName: String,
|
||
|
viewController: UIViewController
|
||
|
) {
|
||
|
guard oldBlockedState != isBlocked else { return }
|
||
|
|
||
|
Storage.shared.writeAsync(
|
||
|
updates: { db in
|
||
|
try Contact
|
||
|
.fetchOrCreate(db, id: threadId)
|
||
|
.with(isBlocked: .updateTo(isBlocked))
|
||
|
.save(db)
|
||
|
},
|
||
|
completion: { db, _ in
|
||
|
try MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete()
|
||
|
|
||
|
DispatchQueue.main.async {
|
||
|
let modal: ConfirmationModal = ConfirmationModal(
|
||
|
info: ConfirmationModal.Info(
|
||
|
title: (oldBlockedState == false ?
|
||
|
"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE".localized() :
|
||
|
String(
|
||
|
format: "BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT".localized(),
|
||
|
displayName
|
||
|
)
|
||
|
),
|
||
|
explanation: (oldBlockedState == false ?
|
||
|
String(
|
||
|
format: "BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT".localized(),
|
||
|
displayName
|
||
|
) :
|
||
|
nil
|
||
|
),
|
||
|
cancelTitle: "BUTTON_OK".localized(),
|
||
|
cancelStyle: .textPrimary
|
||
|
)
|
||
|
)
|
||
|
viewController.present(modal, animated: true)
|
||
|
}
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
}
|