Swiftgram/TelegramUI/UserInfoController.swift
2017-03-23 21:27:34 +03:00

598 lines
25 KiB
Swift

import Foundation
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
private final class UserInfoControllerArguments {
let account: Account
let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void
let openChat: () -> Void
let changeNotificationMuteSettings: () -> Void
let openSharedMedia: () -> Void
let openGroupsInCommon: () -> Void
let updatePeerBlocked: (Bool) -> Void
let deleteContact: () -> Void
init(account: Account, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, openChat: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openGroupsInCommon: @escaping () -> Void, updatePeerBlocked: @escaping (Bool) -> Void, deleteContact: @escaping () -> Void) {
self.account = account
self.updateEditingName = updateEditingName
self.openChat = openChat
self.changeNotificationMuteSettings = changeNotificationMuteSettings
self.openSharedMedia = openSharedMedia
self.openGroupsInCommon = openGroupsInCommon
self.updatePeerBlocked = updatePeerBlocked
self.deleteContact = deleteContact
}
}
private enum UserInfoSection: ItemListSectionId {
case info
case actions
case sharedMediaAndNotifications
case block
}
private enum UserInfoEntry: ItemListNodeEntry {
case info(peer: Peer?, presence: PeerPresence?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState)
case about(text: String)
case phoneNumber(index: Int, value: PhoneNumberWithLabel)
case userName(value: String)
case sendMessage
case shareContact
case startSecretChat
case sharedMedia
case notifications(settings: PeerNotificationSettings?)
case notificationSound(settings: PeerNotificationSettings?)
case groupsInCommon(Int32)
case secretEncryptionKey(SecretChatKeyFingerprint)
case block(action: DestructiveUserInfoAction)
var section: ItemListSectionId {
switch self {
case .info, .about, .phoneNumber, .userName:
return UserInfoSection.info.rawValue
case .sendMessage, .shareContact, .startSecretChat:
return UserInfoSection.actions.rawValue
case .sharedMedia, .notifications, .notificationSound, .secretEncryptionKey, .groupsInCommon:
return UserInfoSection.sharedMediaAndNotifications.rawValue
case .block:
return UserInfoSection.block.rawValue
}
}
var stableId: Int {
return self.sortIndex
}
static func ==(lhs: UserInfoEntry, rhs: UserInfoEntry) -> Bool {
switch lhs {
case let .info(lhsPeer, lhsPresence, lhsCachedData, lhsState):
switch rhs {
case let .info(rhsPeer, rhsPresence, rhsCachedData, rhsState):
if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer {
if !lhsPeer.isEqual(rhsPeer) {
return false
}
} else if (lhsPeer != nil) != (rhsPeer != nil) {
return false
}
if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence {
if !lhsPresence.isEqual(to: rhsPresence) {
return false
}
} else if (lhsPresence != nil) != (rhsPresence != nil) {
return false
}
if let lhsCachedData = lhsCachedData, let rhsCachedData = rhsCachedData {
if !lhsCachedData.isEqual(to: rhsCachedData) {
return false
}
} else if (lhsCachedData != nil) != (rhsCachedData != nil) {
return false
}
if lhsState != rhsState {
return false
}
return true
default:
return false
}
case let .about(lhsText):
switch rhs {
case .about(lhsText):
return true
default:
return false
}
case let .phoneNumber(lhsIndex, lhsValue):
switch rhs {
case let .phoneNumber(rhsIndex, rhsValue) where lhsIndex == rhsIndex && lhsValue == rhsValue:
return true
default:
return false
}
case let .userName(value):
switch rhs {
case .userName(value):
return true
default:
return false
}
case .sendMessage:
switch rhs {
case .sendMessage:
return true
default:
return false
}
case .shareContact:
switch rhs {
case .shareContact:
return true
default:
return false
}
case .startSecretChat:
switch rhs {
case .startSecretChat:
return true
default:
return false
}
case .sharedMedia:
switch rhs {
case .sharedMedia:
return true
default:
return false
}
case let .notifications(lhsSettings):
switch rhs {
case let .notifications(rhsSettings):
if let lhsSettings = lhsSettings, let rhsSettings = rhsSettings {
return lhsSettings.isEqual(to: rhsSettings)
} else if (lhsSettings != nil) != (rhsSettings != nil) {
return false
}
return true
default:
return false
}
case let .notificationSound(lhsSettings):
switch rhs {
case let .notificationSound(rhsSettings):
if let lhsSettings = lhsSettings, let rhsSettings = rhsSettings {
return lhsSettings.isEqual(to: rhsSettings)
} else if (lhsSettings != nil) != (rhsSettings != nil) {
return false
}
return true
default:
return false
}
case let .groupsInCommon(count):
if case .groupsInCommon(count) = rhs {
return true
} else {
return false
}
case let .secretEncryptionKey(fingerprint):
if case .secretEncryptionKey(fingerprint) = rhs {
return true
} else {
return false
}
case let .block(action):
switch rhs {
case .block(action):
return true
default:
return false
}
}
}
private var sortIndex: Int {
switch self {
case .info:
return 0
case .about:
return 1
case let .phoneNumber(index, _):
return 2 + index
case .userName:
return 1000
case .sendMessage:
return 1001
case .shareContact:
return 1002
case .startSecretChat:
return 1003
case .sharedMedia:
return 1004
case .notifications:
return 1005
case .notificationSound:
return 1006
case .groupsInCommon:
return 1007
case .secretEncryptionKey:
return 1008
case .block:
return 1009
}
}
static func <(lhs: UserInfoEntry, rhs: UserInfoEntry) -> Bool {
return lhs.sortIndex < rhs.sortIndex
}
func item(_ arguments: UserInfoControllerArguments) -> ListViewItem {
switch self {
case let .info(peer, presence, cachedData, state):
return ItemListAvatarAndNameInfoItem(account: arguments.account, peer: peer, presence: presence, cachedData: cachedData, state: state, sectionId: self.section, style: .plain, editingNameUpdated: { editingName in
arguments.updateEditingName(editingName)
})
case let .about(text):
return ItemListTextWithLabelItem(label: "about", text: text, multiline: true, sectionId: self.section)
case let .phoneNumber(_, value):
return ItemListTextWithLabelItem(label: value.label, text: formatPhoneNumber(value.number), multiline: false, sectionId: self.section)
case let .userName(value):
return ItemListTextWithLabelItem(label: "username", text: "@\(value)", multiline: false, sectionId: self.section)
case .sendMessage:
return ItemListActionItem(title: "Send Message", kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: {
arguments.openChat()
})
case .shareContact:
return ItemListActionItem(title: "Share Contact", kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: {
})
case .startSecretChat:
return ItemListActionItem(title: "Start Secret Chat", kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: {
})
case .sharedMedia:
return ItemListDisclosureItem(title: "Shared Media", label: "", sectionId: self.section, style: .plain, action: {
arguments.openSharedMedia()
})
case let .notifications(settings):
let label: String
if let settings = settings as? TelegramPeerNotificationSettings, case .muted = settings.muteState {
label = "Disabled"
} else {
label = "Enabled"
}
return ItemListDisclosureItem(title: "Notifications", label: label, sectionId: self.section, style: .plain, action: {
arguments.changeNotificationMuteSettings()
})
case let .notificationSound(settings):
let label: String
label = "Default"
return ItemListDisclosureItem(title: "Sound", label: label, sectionId: self.section, style: .plain, action: {
})
case let .groupsInCommon(count):
return ItemListDisclosureItem(title: "Groups in Common", label: "\(count)", sectionId: self.section, style: .plain, action: {
arguments.openGroupsInCommon()
})
case let .secretEncryptionKey(fingerprint):
return ItemListDisclosureItem(title: "Encryption Key", label: "", sectionId: self.section, style: .plain, action: {
})
case let .block(action):
let title: String
switch action {
case .block:
title = "Block User"
case .unblock:
title = "Unblock User"
case .removeContact:
title = "Remove Contact"
}
return ItemListActionItem(title: title, kind: .destructive, alignment: .natural, sectionId: self.section, style: .plain, action: {
switch action {
case .block:
arguments.updatePeerBlocked(true)
case .unblock:
arguments.updatePeerBlocked(false)
case .removeContact:
arguments.deleteContact()
}
})
}
}
}
private enum DestructiveUserInfoAction {
case block
case removeContact
case unblock
}
private struct UserInfoEditingState: Equatable {
let editingName: ItemListAvatarAndNameInfoItemName?
static func ==(lhs: UserInfoEditingState, rhs: UserInfoEditingState) -> Bool {
if lhs.editingName != rhs.editingName {
return false
}
return true
}
}
private struct UserInfoState: Equatable {
let savingData: Bool
let editingState: UserInfoEditingState?
init() {
self.savingData = false
self.editingState = nil
}
init(savingData: Bool, editingState: UserInfoEditingState?) {
self.savingData = savingData
self.editingState = editingState
}
static func ==(lhs: UserInfoState, rhs: UserInfoState) -> Bool {
if lhs.savingData != rhs.savingData {
return false
}
if lhs.editingState != rhs.editingState {
return false
}
return true
}
func withUpdatedSavingData(_ savingData: Bool) -> UserInfoState {
return UserInfoState(savingData: savingData, editingState: self.editingState)
}
func withUpdatedEditingState(_ editingState: UserInfoEditingState?) -> UserInfoState {
return UserInfoState(savingData: self.savingData, editingState: editingState)
}
}
private func userInfoEntries(account: Account, view: PeerView, state: UserInfoState, peerChatState: Coding?) -> [UserInfoEntry] {
var entries: [UserInfoEntry] = []
guard let peer = view.peers[view.peerId], let user = peerViewMainPeer(view) as? TelegramUser else {
return []
}
var editingName: ItemListAvatarAndNameInfoItemName?
var isEditing = false
if let editingState = state.editingState {
isEditing = true
if view.peerIsContact {
editingName = editingState.editingName
}
}
entries.append(UserInfoEntry.info(peer: user, presence: view.peerPresences[user.id], cachedData: view.cachedData, state: ItemListAvatarAndNameInfoItemState(editingName: editingName, updatingName: nil)))
if let cachedUserData = view.cachedData as? CachedUserData {
if let about = cachedUserData.about, !about.isEmpty {
entries.append(UserInfoEntry.about(text: about))
}
}
if let phoneNumber = user.phone, !phoneNumber.isEmpty {
entries.append(UserInfoEntry.phoneNumber(index: 0, value: PhoneNumberWithLabel(label: "home", number: phoneNumber)))
}
if !isEditing {
if let username = user.username, !username.isEmpty {
entries.append(UserInfoEntry.userName(value: username))
}
if !(peer is TelegramSecretChat) {
entries.append(UserInfoEntry.sendMessage)
if view.peerIsContact {
entries.append(UserInfoEntry.shareContact)
}
entries.append(UserInfoEntry.startSecretChat)
}
entries.append(UserInfoEntry.sharedMedia)
}
entries.append(UserInfoEntry.notifications(settings: view.notificationSettings))
if let groupsInCommon = (view.cachedData as? CachedUserData)?.commonGroupCount, groupsInCommon != 0 && !isEditing {
entries.append(UserInfoEntry.groupsInCommon(groupsInCommon))
}
if peer is TelegramSecretChat, let peerChatState = peerChatState as? SecretChatKeyState, let keyFingerprint = peerChatState.keyFingerprint {
entries.append(UserInfoEntry.secretEncryptionKey(keyFingerprint))
}
if isEditing {
entries.append(UserInfoEntry.notificationSound(settings: view.notificationSettings))
if view.peerIsContact {
entries.append(UserInfoEntry.block(action: .removeContact))
}
} else {
if let cachedData = view.cachedData as? CachedUserData {
if cachedData.isBlocked {
entries.append(UserInfoEntry.block(action: .unblock))
} else {
entries.append(UserInfoEntry.block(action: .block))
}
}
}
return entries
}
public func userInfoController(account: Account, peerId: PeerId) -> ViewController {
let statePromise = ValuePromise(UserInfoState(), ignoreRepeated: true)
let stateValue = Atomic(value: UserInfoState())
let updateState: ((UserInfoState) -> UserInfoState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var pushControllerImpl: ((ViewController) -> Void)?
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments) -> Void)?
var openChatImpl: (() -> Void)?
let actionsDisposable = DisposableSet()
if peerId.namespace == Namespaces.Peer.CloudChannel {
actionsDisposable.add(account.viewTracker.updatedCachedChannelParticipants(peerId, forceImmediateUpdate: true).start())
}
let updatePeerNameDisposable = MetaDisposable()
actionsDisposable.add(updatePeerNameDisposable)
let updatePeerBlockedDisposable = MetaDisposable()
actionsDisposable.add(updatePeerBlockedDisposable)
let changeMuteSettingsDisposable = MetaDisposable()
actionsDisposable.add(changeMuteSettingsDisposable)
let arguments = UserInfoControllerArguments(account: account, updateEditingName: { editingName in
updateState { state in
if let _ = state.editingState {
return state.withUpdatedEditingState(UserInfoEditingState(editingName: editingName))
} else {
return state
}
}
}, openChat: {
openChatImpl?()
}, changeNotificationMuteSettings: {
let controller = ActionSheetController()
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
let notificationAction: (Int32) -> Void = { muteUntil in
let muteState: PeerMuteState
if muteUntil <= 0 {
muteState = .unmuted
} else if muteUntil == Int32.max {
muteState = .muted(until: Int32.max)
} else {
muteState = .muted(until: Int32(Date().timeIntervalSince1970) + muteUntil)
}
changeMuteSettingsDisposable.set(changePeerNotificationSettings(account: account, peerId: peerId, settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: PeerMessageSound.bundledModern(id: 0))).start())
}
controller.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: "Enable", action: {
dismissAction()
notificationAction(0)
}),
ActionSheetButtonItem(title: "Mute for 1 hour", action: {
dismissAction()
notificationAction(1 * 60 * 60)
}),
ActionSheetButtonItem(title: "Mute for 8 hours", action: {
dismissAction()
notificationAction(8 * 60 * 60)
}),
ActionSheetButtonItem(title: "Mute for 2 days", action: {
dismissAction()
notificationAction(2 * 24 * 60 * 60)
}),
ActionSheetButtonItem(title: "Disable", action: {
dismissAction()
notificationAction(Int32.max)
})
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: "Cancel", action: { dismissAction() })])
])
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}, openSharedMedia: {
if let controller = peerSharedMediaController(account: account, peerId: peerId) {
pushControllerImpl?(controller)
}
}, openGroupsInCommon: {
pushControllerImpl?(groupsInCommonController(account: account, peerId: peerId))
}, updatePeerBlocked: { value in
updatePeerBlockedDisposable.set(requestUpdatePeerIsBlocked(account: account, peerId: peerId, isBlocked: value).start())
}, deleteContact: {
})
let signal = combineLatest(statePromise.get(), account.viewTracker.peerView(peerId), account.postbox.combinedView(keys: [.peerChatState(peerId: peerId)]))
|> map { state, view, chatState -> (ItemListControllerState, (ItemListNodeState<UserInfoEntry>, UserInfoEntry.ItemGenerationArguments)) in
let peer = peerViewMainPeer(view)
var leftNavigationButton: ItemListNavigationButton?
let rightNavigationButton: ItemListNavigationButton
if let editingState = state.editingState {
leftNavigationButton = ItemListNavigationButton(title: "Cancel", style: .regular, enabled: true, action: {
updateState {
$0.withUpdatedEditingState(nil)
}
})
var doneEnabled = true
if let editingName = editingState.editingName, editingName.isEmpty {
doneEnabled = false
}
if state.savingData {
rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: doneEnabled, action: {})
} else {
rightNavigationButton = ItemListNavigationButton(title: "Done", style: .bold, enabled: doneEnabled, action: {
var updateName: ItemListAvatarAndNameInfoItemName?
updateState { state in
if let editingState = state.editingState, let editingName = editingState.editingName {
if let user = peer {
if ItemListAvatarAndNameInfoItemName(user.indexName) != editingName {
updateName = editingName
}
}
}
if updateName != nil {
return state.withUpdatedSavingData(true)
} else {
return state.withUpdatedEditingState(nil)
}
}
if let updateName = updateName, case let .personName(firstName, lastName) = updateName {
updatePeerNameDisposable.set((updateContactName(account: account, peerId: peerId, firstName: firstName, lastName: lastName) |> deliverOnMainQueue).start(error: { _ in
updateState { state in
return state.withUpdatedSavingData(false)
}
}, completed: {
updateState { state in
return state.withUpdatedSavingData(false).withUpdatedEditingState(nil)
}
}))
}
})
}
} else {
rightNavigationButton = ItemListNavigationButton(title: "Edit", style: .regular, enabled: true, action: {
if let user = peer {
updateState { state in
return state.withUpdatedEditingState(UserInfoEditingState(editingName: ItemListAvatarAndNameInfoItemName(user.indexName)))
}
}
})
}
let controllerState = ItemListControllerState(title: "Info", leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton)
let listState = ItemListNodeState(entries: userInfoEntries(account: account, view: view, state: state, peerChatState: (chatState.views[.peerChatState(peerId: peerId)] as? PeerChatStateView)?.chatState), style: .plain)
return (controllerState, (listState, arguments))
} |> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(signal)
pushControllerImpl = { [weak controller] value in
(controller?.navigationController as? NavigationController)?.pushViewController(value)
}
presentControllerImpl = { [weak controller] value, presentationArguments in
controller?.present(value, in: .window, with: presentationArguments)
}
openChatImpl = { [weak controller] in
if let navigationController = (controller?.navigationController as? NavigationController) {
navigateToChatController(navigationController: navigationController, account: account, peerId: peerId)
}
}
return controller
}