Swiftgram/submodules/TelegramUI/TelegramUI/ChannelDiscussionGroupSetupController.swift
2019-06-19 00:32:27 +02:00

615 lines
32 KiB
Swift

import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
private final class ChannelDiscussionGroupSetupControllerArguments {
let account: Account
let createGroup: () -> Void
let selectGroup: (PeerId) -> Void
let unlinkGroup: () -> Void
init(account: Account, createGroup: @escaping () -> Void, selectGroup: @escaping (PeerId) -> Void, unlinkGroup: @escaping () -> Void) {
self.account = account
self.createGroup = createGroup
self.selectGroup = selectGroup
self.unlinkGroup = unlinkGroup
}
}
private enum ChannelDiscussionGroupSetupControllerSection: Int32 {
case header
case groups
case unlink
}
private enum ChannelDiscussionGroupSetupControllerEntryStableId: Hashable {
case id(Int)
case peer(PeerId)
}
private enum ChannelDiscussionGroupSetupControllerEntry: ItemListNodeEntry {
case header(PresentationTheme, PresentationStrings, String?, Bool, String)
case create(PresentationTheme, String)
case group(Int, PresentationTheme, PresentationStrings, Peer, PresentationPersonNameOrder)
case groupsInfo(PresentationTheme, String)
case unlink(PresentationTheme, String)
var section: Int32 {
switch self {
case .header:
return ChannelDiscussionGroupSetupControllerSection.header.rawValue
case .create, .group, .groupsInfo:
return ChannelDiscussionGroupSetupControllerSection.groups.rawValue
case .unlink:
return ChannelDiscussionGroupSetupControllerSection.unlink.rawValue
}
}
var stableId: ChannelDiscussionGroupSetupControllerEntryStableId {
switch self {
case .header:
return .id(0)
case .create:
return .id(1)
case let .group(_, _, _, peer, _):
return .peer(peer.id)
case .groupsInfo:
return .id(2)
case .unlink:
return .id(3)
}
}
static func ==(lhs: ChannelDiscussionGroupSetupControllerEntry, rhs: ChannelDiscussionGroupSetupControllerEntry) -> Bool {
switch lhs {
case let .header(lhsTheme, lhsStrings, lhsTitle, lhsIsGroup, lhsLabel):
if case let .header(rhsTheme, rhsStrings, rhsTitle, rhsIsGroup, rhsLabel) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsTitle == rhsTitle, lhsIsGroup == rhsIsGroup, lhsLabel == rhsLabel {
return true
} else {
return false
}
case let .create(lhsTheme, lhsTitle):
if case let .create(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle {
return true
} else {
return false
}
case let .group(lhsIndex, lhsTheme, lhsStrings, lhsPeer, lhsNameOrder):
if case let .group(rhsIndex, rhsTheme, rhsStrings, rhsPeer, rhsNameOrder) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings == rhsStrings, lhsPeer.isEqual(rhsPeer), lhsNameOrder == rhsNameOrder {
return true
} else {
return false
}
case let .groupsInfo(lhsTheme, lhsTitle):
if case let .groupsInfo(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle {
return true
} else {
return false
}
case let .unlink(lhsTheme, lhsTitle):
if case let .unlink(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle {
return true
} else {
return false
}
}
}
private var sortIndex: Int {
switch self {
case .header:
return 0
case .create:
return 1
case let .group(index, _, _, _, _):
return 10 + index
case .groupsInfo:
return 1000
case .unlink:
return 1001
}
}
static func <(lhs: ChannelDiscussionGroupSetupControllerEntry, rhs: ChannelDiscussionGroupSetupControllerEntry) -> Bool {
return lhs.sortIndex < rhs.sortIndex
}
func item(_ arguments: ChannelDiscussionGroupSetupControllerArguments) -> ListViewItem {
switch self {
case let .header(theme, strings, title, isGroup, label):
return ChannelDiscussionGroupSetupHeaderItem(theme: theme, strings: strings, title: title, isGroup: isGroup, label: label, sectionId: self.section)
case let .create(theme, text):
return ItemListPeerActionItem(theme: theme, icon: PresentationResourcesItemList.plusIconImage(theme), title: text, sectionId: self.section, editing: false, action: {
arguments.createGroup()
})
case let .group(_, theme, strings, peer, nameOrder):
let text: String
if let peer = peer as? TelegramChannel, let addressName = peer.addressName, !addressName.isEmpty {
text = "@\(addressName)"
} else {
text = strings.Channel_DiscussionGroup_PrivateGroup
}
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .regular, dateFormat: .monthFirst, dateSeparator: ".", decimalSeparator: ".", groupingSeparator: "."), nameDisplayOrder: nameOrder, account: arguments.account, peer: peer, aliasHandling: .standard, nameStyle: .plain, presence: nil, text: .text(text), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: {
arguments.selectGroup(peer.id)
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in })
case let .groupsInfo(theme, title):
return ItemListTextItem(theme: theme, text: .plain(title), sectionId: self.section)
case let .unlink(theme, title):
return ItemListActionItem(theme: theme, title: title, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: {
arguments.unlinkGroup()
})
}
}
}
private func channelDiscussionGroupSetupControllerEntries(presentationData: PresentationData, view: PeerView, groups: [Peer]?) -> [ChannelDiscussionGroupSetupControllerEntry] {
guard let peer = view.peers[view.peerId] as? TelegramChannel, let cachedData = view.cachedData as? CachedChannelData else {
return []
}
let canEditChannel = peer.hasPermission(.changeInfo)
var entries: [ChannelDiscussionGroupSetupControllerEntry] = []
if let linkedDiscussionPeerId = cachedData.linkedDiscussionPeerId {
if let group = view.peers[linkedDiscussionPeerId] {
if case .group = peer.info {
entries.append(.header(presentationData.theme, presentationData.strings, group.displayTitle, true, presentationData.strings.Channel_DiscussionGroup_HeaderLabel))
} else {
entries.append(.header(presentationData.theme, presentationData.strings, group.displayTitle, false, presentationData.strings.Channel_DiscussionGroup_HeaderLabel))
}
entries.append(.group(0, presentationData.theme, presentationData.strings, group, presentationData.nameDisplayOrder))
entries.append(.groupsInfo(presentationData.theme, presentationData.strings.Channel_DiscussionGroup_Info))
if canEditChannel {
let unlinkText: String
if case .group = peer.info {
unlinkText = presentationData.strings.Channel_DiscussionGroup_UnlinkChannel
} else {
unlinkText = presentationData.strings.Channel_DiscussionGroup_UnlinkGroup
}
entries.append(.unlink(presentationData.theme, unlinkText))
}
}
} else if case .broadcast = peer.info, canEditChannel {
if let groups = groups {
entries.append(.header(presentationData.theme, presentationData.strings, nil, true, presentationData.strings.Channel_DiscussionGroup_HeaderLabel))
entries.append(.create(presentationData.theme, presentationData.strings.Channel_DiscussionGroup_Create))
var index = 0
for group in groups {
entries.append(.group(index, presentationData.theme, presentationData.strings, group, presentationData.nameDisplayOrder))
index += 1
}
entries.append(.groupsInfo(presentationData.theme, presentationData.strings.Channel_DiscussionGroup_Info))
}
}
return entries
}
private struct ChannelDiscussionGroupSetupControllerState: Equatable {
var searching: Bool = false
}
public func channelDiscussionGroupSetupController(context: AccountContext, peerId: PeerId) -> ViewController {
let statePromise = ValuePromise(ChannelDiscussionGroupSetupControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: ChannelDiscussionGroupSetupControllerState())
let updateState: ((ChannelDiscussionGroupSetupControllerState) -> ChannelDiscussionGroupSetupControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
let groupPeers = Promise<[Peer]?>()
groupPeers.set(.single(nil)
|> then(
availableGroupsForChannelDiscussion(postbox: context.account.postbox, network: context.account.network)
|> map(Optional.init)
|> `catch` { _ -> Signal<[Peer]?, NoError> in
return .single(nil)
}
))
let peerView = context.account.viewTracker.peerView(peerId)
var dismissImpl: (() -> Void)?
var dismissInputImpl: (() -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
var presentControllerImpl: ((ViewController, Any?) -> Void)?
var navigateToGroupImpl: ((PeerId) -> Void)?
let actionsDisposable = DisposableSet()
let applyGroupDisposable = MetaDisposable()
actionsDisposable.add(applyGroupDisposable)
let arguments = ChannelDiscussionGroupSetupControllerArguments(account: context.account, createGroup: {
let _ = (context.account.postbox.transaction { transaction -> Peer? in
transaction.getPeer(peerId)
}
|> deliverOnMainQueue).start(next: { peer in
guard let peer = peer else {
return
}
pushControllerImpl?(createGroupController(context: context, peerIds: [], initialTitle: peer.displayTitle + " Chat", mode: .supergroup, completion: { groupId, dismiss in
var applySignal = updateGroupDiscussionForChannel(network: context.account.network, postbox: context.account.postbox, channelId: peerId, groupId: groupId)
var cancelImpl: (() -> Void)?
let progressSignal = Signal<Never, NoError> { subscriber in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = OverlayStatusController(theme: presentationData.theme, strings: presentationData.strings, type: .loading(cancelled: {
cancelImpl?()
}))
presentControllerImpl?(controller, nil)
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
applySignal = applySignal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
cancelImpl = {
applyGroupDisposable.set(nil)
}
applyGroupDisposable.set((applySignal
|> deliverOnMainQueue).start(error: { _ in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
dismiss()
}, completed: {
dismiss()
/*let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = OverlayStatusController(theme: presentationData.theme, strings: presentationData.strings, type: .success)
presentControllerImpl?(controller, nil)*/
}))
}))
})
}, selectGroup: { groupId in
dismissInputImpl?()
let _ = (context.account.postbox.transaction { transaction -> (CachedChannelData?, Peer?, Peer?) in
return (transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData, transaction.getPeer(peerId), transaction.getPeer(groupId))
}
|> deliverOnMainQueue).start(next: { cachedData, channelPeer, groupPeer in
guard let cachedData = cachedData, let channelPeer = channelPeer, let groupPeer = groupPeer else {
return
}
if groupId == cachedData.linkedDiscussionPeerId {
navigateToGroupImpl?(groupId)
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = ActionSheetController(presentationTheme: presentationData.theme)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ChannelDiscussionGroupActionSheetItem(context: context, channelPeer: channelPeer, groupPeer: groupPeer, strings: presentationData.strings),
ActionSheetButtonItem(title: presentationData.strings.Channel_DiscussionGroup_LinkGroup, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
var applySignal: Signal<Bool, ChannelDiscussionGroupError>
var updatedPeerId: PeerId? = nil
if let legacyGroup = groupPeer as? TelegramGroup {
applySignal = convertGroupToSupergroup(account: context.account, peerId: legacyGroup.id)
|> mapError { _ -> ChannelDiscussionGroupError in
return .generic
}
|> deliverOnMainQueue
|> mapToSignal { resultPeerId -> Signal<Bool, ChannelDiscussionGroupError> in
updatedPeerId = resultPeerId
return context.account.postbox.transaction { transaction -> Signal<Bool, ChannelDiscussionGroupError> in
if let groupPeer = transaction.getPeer(resultPeerId) {
let _ = (groupPeers.get()
|> take(1)
|> deliverOnMainQueue).start(next: { groups in
guard var groups = groups else {
return
}
for i in 0 ..< groups.count {
if groups[i].id == groupId {
groups[i] = groupPeer
break
}
}
groupPeers.set(.single(groups))
})
}
return updateGroupDiscussionForChannel(network: context.account.network, postbox: context.account.postbox, channelId: peerId, groupId: resultPeerId)
}
|> introduceError(ChannelDiscussionGroupError.self)
|> switchToLatest
}
} else {
applySignal = updateGroupDiscussionForChannel(network: context.account.network, postbox: context.account.postbox, channelId: peerId, groupId: groupId)
}
var cancelImpl: (() -> Void)?
let progressSignal = Signal<Never, NoError> { subscriber in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = OverlayStatusController(theme: presentationData.theme, strings: presentationData.strings, type: .loading(cancelled: {
cancelImpl?()
}))
presentControllerImpl?(controller, nil)
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
applySignal = applySignal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
cancelImpl = {
applyGroupDisposable.set(nil)
}
applyGroupDisposable.set((applySignal
|> deliverOnMainQueue).start(error: { error in
switch error {
case .generic, .hasNotPermissions:
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
updateState { state in
var state = state
state.searching = false
return state
}
case .hasNotPermissions:
//TODO process error
break
case .groupHistoryIsCurrentlyPrivate:
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Channel_DiscussionGroup_MakeHistoryPublic, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Channel_DiscussionGroup_MakeHistoryPublicProceed, action: {
var applySignal: Signal<Bool, ChannelDiscussionGroupError> = updateChannelHistoryAvailabilitySettingsInteractively(postbox: context.account.postbox, network: context.account.network, accountStateManager: context.account.stateManager, peerId: updatedPeerId ?? groupId, historyAvailableForNewMembers: true)
|> mapError { _ -> ChannelDiscussionGroupError in
return .generic
}
|> mapToSignal { _ -> Signal<Bool, ChannelDiscussionGroupError> in
return .complete()
}
|> then(
updateGroupDiscussionForChannel(network: context.account.network, postbox: context.account.postbox, channelId: peerId, groupId: updatedPeerId ?? groupId)
)
var cancelImpl: (() -> Void)?
let progressSignal = Signal<Never, NoError> { subscriber in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = OverlayStatusController(theme: presentationData.theme, strings: presentationData.strings, type: .loading(cancelled: {
cancelImpl?()
}))
presentControllerImpl?(controller, nil)
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
applySignal = applySignal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
cancelImpl = {
applyGroupDisposable.set(nil)
}
applyGroupDisposable.set((applySignal
|> deliverOnMainQueue).start(error: { _ in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
updateState { state in
var state = state
state.searching = false
return state
}
}, completed: {
updateState { state in
var state = state
state.searching = false
return state
}
}))
})]), nil)
}
}, completed: {
updateState { state in
var state = state
state.searching = false
return state
}
}))
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
presentControllerImpl?(actionSheet, nil)
})
}, unlinkGroup: {
let _ = (context.account.postbox.transaction { transaction -> (CachedChannelData?, Peer?) in
return (transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData, transaction.getPeer(peerId))
}
|> deliverOnMainQueue).start(next: { cachedData, peer in
guard let cachedData = cachedData, let peer = peer as? TelegramChannel else {
return
}
let applyPeerId: PeerId
if case .broadcast = peer.info {
applyPeerId = peerId
} else if let linkedDiscussionPeerId = cachedData.linkedDiscussionPeerId {
applyPeerId = linkedDiscussionPeerId
} else {
return
}
var applySignal = updateGroupDiscussionForChannel(network: context.account.network, postbox: context.account.postbox, channelId: applyPeerId, groupId: nil)
var cancelImpl: (() -> Void)?
let progressSignal = Signal<Never, NoError> { subscriber in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = OverlayStatusController(theme: presentationData.theme, strings: presentationData.strings, type: .loading(cancelled: {
cancelImpl?()
}))
presentControllerImpl?(controller, nil)
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
applySignal = applySignal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
cancelImpl = {
applyGroupDisposable.set(nil)
}
applyGroupDisposable.set((applySignal
|> deliverOnMainQueue).start(error: { _ in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
}, completed: {
if case .group = peer.info {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = OverlayStatusController(theme: presentationData.theme, strings: presentationData.strings, type: .success)
presentControllerImpl?(controller, nil)
dismissImpl?()
}
}))
})
})
var wasEmpty: Bool?
let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get(), peerView, groupPeers.get())
|> deliverOnMainQueue
|> map { presentationData, state, view, groups -> (ItemListControllerState, (ItemListNodeState<ChannelDiscussionGroupSetupControllerEntry>, ChannelDiscussionGroupSetupControllerEntry.ItemGenerationArguments)) in
let title: String
if let peer = view.peers[view.peerId] as? TelegramChannel, case .broadcast = peer.info {
title = presentationData.strings.Channel_DiscussionGroup
} else {
title = presentationData.strings.Group_LinkedChannel
}
var crossfade = false
var isEmptyState = false
var displayGroupList = false
if let cachedData = view.cachedData as? CachedChannelData {
let isEmpty = cachedData.linkedDiscussionPeerId == nil
if let peer = view.peers[view.peerId] as? TelegramChannel, case .broadcast = peer.info {
if cachedData.linkedDiscussionPeerId == nil {
if groups == nil {
isEmptyState = true
} else {
displayGroupList = true
}
}
}
if let wasEmpty = wasEmpty, wasEmpty != isEmpty {
crossfade = true
}
wasEmpty = isEmpty
} else {
isEmptyState = true
}
var emptyStateItem: ItemListControllerEmptyStateItem?
if isEmptyState {
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme)
}
var rightNavigationButton: ItemListNavigationButton?
var searchItem: ItemListControllerSearch?
if let groups = groups, groups.count >= 10, displayGroupList {
if state.searching {
searchItem = ChannelDiscussionGroupSetupSearchItem(context: context, peers: groups, cancel: {
updateState { state in
var state = state
state.searching = false
return state
}
}, dismissInput: {
dismissInputImpl?()
}, openPeer: { peer in
arguments.selectGroup(peer.id)
})
} else {
rightNavigationButton = ItemListNavigationButton(content: .icon(.search), style: .regular, enabled: true, action: {
updateState { state in
var state = state
state.searching = true
return state
}
})
}
}
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(entries: channelDiscussionGroupSetupControllerEntries(presentationData: presentationData, view: view, groups: groups), style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, crossfadeState: crossfade, animateChanges: false)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
dismissImpl = { [weak controller] in
if let controller = controller {
(controller.navigationController as? NavigationController)?.filterController(controller, animated: true)
}
}
dismissInputImpl = { [weak controller] in
controller?.view.endEditing(true)
}
pushControllerImpl = { [weak controller] c in
(controller?.navigationController as? NavigationController)?.pushViewController(c)
}
presentControllerImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a)
}
navigateToGroupImpl = { [weak controller] groupId in
guard let navigationController = controller?.navigationController as? NavigationController else {
return
}
navigateToChatController(navigationController: navigationController, context: context, chatLocation: .peer(groupId), keepStack: .always)
}
return controller
}