Swiftgram/submodules/PeerInfoUI/Sources/ChannelInfoController.swift
2019-10-21 16:58:00 +04:00

1197 lines
63 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import SyncCore
import LegacyComponents
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AccountContext
import TextFormat
import OverlayStatusController
import TelegramStringFormatting
import ShareController
import AlertUI
import PresentationDataUtils
import GalleryUI
import LegacyUI
import ItemListAvatarAndNameInfoItem
import WebSearchUI
import PeerAvatarGalleryUI
import NotificationMuteSettingsUI
import MapResourceToAvatarSizes
import NotificationSoundSelectionUI
import Markdown
private final class ChannelInfoControllerArguments {
let account: Account
let avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext
let tapAvatarAction: () -> Void
let changeProfilePhoto: () -> Void
let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void
let updateEditingDescriptionText: (String) -> Void
let openChannelTypeSetup: () -> Void
let openDiscussionGroupSetup: () -> Void
let changeNotificationMuteSettings: () -> Void
let openSharedMedia: () -> Void
let openStats: () -> Void
let openAdmins: () -> Void
let openMembers: () -> Void
let openBanned: () -> Void
let reportChannel: () -> Void
let leaveChannel: () -> Void
let deleteChannel: () -> Void
let displayAddressNameContextMenu: (String) -> Void
let displayContextMenu: (ChannelInfoEntryTag, String) -> Void
let aboutLinkAction: (TextLinkItemActionType, TextLinkItem) -> Void
let toggleSignatures:(Bool) -> Void
init(account: Account, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, tapAvatarAction: @escaping () -> Void, changeProfilePhoto: @escaping () -> Void, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updateEditingDescriptionText: @escaping (String) -> Void, openChannelTypeSetup: @escaping () -> Void, openDiscussionGroupSetup: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openStats: @escaping () -> Void, openAdmins: @escaping () -> Void, openMembers: @escaping () -> Void, openBanned: @escaping () -> Void, reportChannel: @escaping () -> Void, leaveChannel: @escaping () -> Void, deleteChannel: @escaping () -> Void, displayAddressNameContextMenu: @escaping (String) -> Void, displayContextMenu: @escaping (ChannelInfoEntryTag, String) -> Void, aboutLinkAction: @escaping (TextLinkItemActionType, TextLinkItem) -> Void, toggleSignatures: @escaping(Bool)->Void) {
self.account = account
self.avatarAndNameInfoContext = avatarAndNameInfoContext
self.tapAvatarAction = tapAvatarAction
self.changeProfilePhoto = changeProfilePhoto
self.updateEditingName = updateEditingName
self.updateEditingDescriptionText = updateEditingDescriptionText
self.openChannelTypeSetup = openChannelTypeSetup
self.openDiscussionGroupSetup = openDiscussionGroupSetup
self.changeNotificationMuteSettings = changeNotificationMuteSettings
self.openSharedMedia = openSharedMedia
self.openStats = openStats
self.openAdmins = openAdmins
self.openMembers = openMembers
self.openBanned = openBanned
self.reportChannel = reportChannel
self.leaveChannel = leaveChannel
self.deleteChannel = deleteChannel
self.displayAddressNameContextMenu = displayAddressNameContextMenu
self.displayContextMenu = displayContextMenu
self.aboutLinkAction = aboutLinkAction
self.toggleSignatures = toggleSignatures
}
}
private enum ChannelInfoSection: ItemListSectionId {
case info
case discriptionAndType
case sharedMediaAndNotifications
case sign
case members
case reportOrLeave
}
private enum ChannelInfoEntryTag {
case about
case link
}
private enum ChannelInfoEntry: ItemListNodeEntry {
case info(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, peer: Peer?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState, updatingAvatar: ItemListAvatarAndNameInfoItemUpdatingAvatar?)
case about(theme: PresentationTheme, text: String, value: String)
case addressName(theme: PresentationTheme, text: String, value: String)
case channelPhotoSetup(theme: PresentationTheme, text: String)
case channelTypeSetup(theme: PresentationTheme, text: String, value: String)
case discussionGroupSetup(theme: PresentationTheme, text: String, value: String)
case discussionGroupSetupInfo(theme: PresentationTheme, text: String)
case channelDescriptionSetup(theme: PresentationTheme, placeholder: String, value: String)
case admins(theme: PresentationTheme, text: String, value: String)
case members(theme: PresentationTheme, text: String, value: String)
case banned(theme: PresentationTheme, text: String, value: String)
case notifications(theme: PresentationTheme, text: String, value: String)
case sharedMedia(theme: PresentationTheme, text: String)
case stats(theme: PresentationTheme, text: String)
case signMessages(theme: PresentationTheme, text: String, value: Bool)
case signInfo(theme: PresentationTheme, text: String)
case report(theme: PresentationTheme, text: String)
case leave(theme: PresentationTheme, text: String)
case deleteChannel(theme: PresentationTheme, text: String)
var section: ItemListSectionId {
switch self {
case .info, .about, .addressName, .channelPhotoSetup, .channelDescriptionSetup:
return ChannelInfoSection.info.rawValue
case .channelTypeSetup, .discussionGroupSetup, .discussionGroupSetupInfo:
return ChannelInfoSection.discriptionAndType.rawValue
case .signMessages, .signInfo:
return ChannelInfoSection.sign.rawValue
case .admins, .members, .banned:
return ChannelInfoSection.members.rawValue
case .sharedMedia, .notifications, .stats:
return ChannelInfoSection.sharedMediaAndNotifications.rawValue
case .report, .leave, .deleteChannel:
return ChannelInfoSection.reportOrLeave.rawValue
}
}
var stableId: Int32 {
switch self {
case .info:
return 0
case .channelPhotoSetup:
return 1
case .addressName:
return 2
case .about:
return 3
case .channelDescriptionSetup:
return 4
case .channelTypeSetup:
return 5
case .discussionGroupSetup:
return 6
case .discussionGroupSetupInfo:
return 7
case .signMessages:
return 8
case .signInfo:
return 9
case .admins:
return 10
case .members:
return 11
case .banned:
return 12
case .notifications:
return 13
case .sharedMedia:
return 14
case .stats:
return 15
case .report:
return 16
case .leave:
return 17
case .deleteChannel:
return 18
}
}
static func ==(lhs: ChannelInfoEntry, rhs: ChannelInfoEntry) -> Bool {
switch lhs {
case let .info(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsPeer, lhsCachedData, lhsState, lhsUpdatingAvatar):
if case let .info(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsPeer, rhsCachedData, rhsState, rhsUpdatingAvatar) = rhs {
if lhsTheme !== rhsTheme {
return false
}
if lhsStrings !== rhsStrings {
return false
}
if lhsDateTimeFormat != rhsDateTimeFormat {
return false
}
if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer {
if !lhsPeer.isEqual(rhsPeer) {
return false
}
} else if (lhsPeer == nil) != (rhsPeer != 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
}
if lhsUpdatingAvatar != rhsUpdatingAvatar {
return false
}
return true
} else {
return false
}
case let .about(lhsTheme, lhsText, lhsValue):
if case let .about(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .addressName(lhsTheme, lhsText, lhsValue):
if case let .addressName(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .channelPhotoSetup(lhsTheme, lhsText):
if case let .channelPhotoSetup(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .channelTypeSetup(lhsTheme, lhsText, lhsValue):
if case let .channelTypeSetup(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .discussionGroupSetup(lhsTheme, lhsText, lhsValue):
if case let .discussionGroupSetup(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .discussionGroupSetupInfo(lhsTheme, lhsText):
if case let .discussionGroupSetupInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .channelDescriptionSetup(lhsTheme, lhsPlaceholder, lhsValue):
if case let .channelDescriptionSetup(rhsTheme, rhsPlaceholder, rhsValue) = rhs, lhsTheme === rhsTheme, lhsPlaceholder == rhsPlaceholder, lhsValue == rhsValue {
return true
} else {
return false
}
case let .admins(lhsTheme, lhsText, lhsValue):
if case let .admins(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .members(lhsTheme, lhsText, lhsValue):
if case let .members(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .banned(lhsTheme, lhsText, lhsValue):
if case let .banned(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .signMessages(lhsTheme, lhsText, lhsValue):
if case let .signMessages(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .signInfo(lhsTheme, lhsText):
if case let .signInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .sharedMedia(lhsTheme, lhsText):
if case let .sharedMedia(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .stats(lhsTheme, lhsText):
if case let .stats(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .report(lhsTheme, lhsText):
if case let .report(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .leave(lhsTheme, lhsText):
if case let .leave(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .deleteChannel(lhsTheme, lhsText):
if case let .deleteChannel(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .notifications(lhsTheme, lhsText, lhsValue):
if case let .notifications(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
}
}
static func <(lhs: ChannelInfoEntry, rhs: ChannelInfoEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(_ arguments: Any) -> ListViewItem {
let arguments = arguments as! ChannelInfoControllerArguments
switch self {
case let .info(theme, strings, dateTimeFormat, peer, cachedData, state, updatingAvatar):
return ItemListAvatarAndNameInfoItem(account: arguments.account, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, mode: .generic, peer: peer, presence: nil, cachedData: cachedData, state: state, sectionId: self.section, style: .plain, editingNameUpdated: { editingName in
arguments.updateEditingName(editingName)
}, avatarTapped: {
arguments.tapAvatarAction()
}, context: arguments.avatarAndNameInfoContext, updatingImage: updatingAvatar)
case let .about(theme, text, value):
return ItemListTextWithLabelItem(theme: theme, label: text, text: foldMultipleLineBreaks(value), enabledEntityTypes: [.url, .mention, .hashtag], multiline: true, sectionId: self.section, action: nil, longTapAction: {
arguments.displayContextMenu(ChannelInfoEntryTag.about, value)
}, linkItemAction: { action, itemLink in
arguments.aboutLinkAction(action, itemLink)
}, tag: ChannelInfoEntryTag.about)
case let .addressName(theme, text, value):
return ItemListTextWithLabelItem(theme: theme, label: text, text: "https://t.me/\(value)", textColor: .accent, enabledEntityTypes: [], multiline: false, sectionId: self.section, action: {
arguments.displayAddressNameContextMenu("https://t.me/\(value)")
}, longTapAction: {
arguments.displayContextMenu(ChannelInfoEntryTag.link, "https://t.me/\(value)")
}, tag: ChannelInfoEntryTag.link)
case let .channelPhotoSetup(theme, text):
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: {
arguments.changeProfilePhoto()
})
case let .channelTypeSetup(theme, text, value):
return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .plain, action: {
arguments.openChannelTypeSetup()
})
case let .discussionGroupSetup(theme, text, value):
return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .plain, action: {
arguments.openDiscussionGroupSetup()
})
case let .discussionGroupSetupInfo(theme, text):
return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section)
case let .channelDescriptionSetup(theme, placeholder, value):
return ItemListMultilineInputItem(theme: theme, text: value, placeholder: placeholder, maxLength: ItemListMultilineInputItemTextLimit(value: 255, display: true), sectionId: self.section, style: .plain, textUpdated: { updatedText in
arguments.updateEditingDescriptionText(updatedText)
})
case let .admins(theme, text, value):
return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .plain, action: {
arguments.openAdmins()
})
case let .members(theme, text, value):
return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .plain, action: {
arguments.openMembers()
})
case let .banned(theme, text, value):
return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .plain, action: {
arguments.openBanned()
})
case let .signMessages(theme, text, value):
return ItemListSwitchItem(theme: theme, title: text, value: value, sectionId: self.section, style: .plain, updated: { updated in
arguments.toggleSignatures(updated)
})
case let .signInfo(theme, text):
return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section, style: .plain)
case let .sharedMedia(theme, text):
return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .plain, action: {
arguments.openSharedMedia()
})
case let .stats(theme, text):
return ItemListDisclosureItem(theme: theme, title: text, label: "", sectionId: self.section, style: .plain, action: {
arguments.openStats()
})
case let .notifications(theme, text, value):
return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .plain, action: {
arguments.changeNotificationMuteSettings()
})
case let .report(theme, text):
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: {
arguments.reportChannel()
})
case let .leave(theme, text):
return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .plain, action: {
arguments.leaveChannel()
})
case let .deleteChannel(theme, text):
return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .plain, action: {
arguments.deleteChannel()
})
}
}
}
private struct ChannelInfoState: Equatable {
let updatingAvatar: ItemListAvatarAndNameInfoItemUpdatingAvatar?
let editingState: ChannelInfoEditingState?
let savingData: Bool
init(updatingAvatar: ItemListAvatarAndNameInfoItemUpdatingAvatar?, editingState: ChannelInfoEditingState?, savingData: Bool) {
self.updatingAvatar = updatingAvatar
self.editingState = editingState
self.savingData = savingData
}
init() {
self.updatingAvatar = nil
self.editingState = nil
self.savingData = false
}
static func ==(lhs: ChannelInfoState, rhs: ChannelInfoState) -> Bool {
if lhs.updatingAvatar != rhs.updatingAvatar {
return false
}
if lhs.editingState != rhs.editingState {
return false
}
if lhs.savingData != rhs.savingData {
return false
}
return true
}
func withUpdatedUpdatingAvatar(_ updatingAvatar: ItemListAvatarAndNameInfoItemUpdatingAvatar?) -> ChannelInfoState {
return ChannelInfoState(updatingAvatar: updatingAvatar, editingState: self.editingState, savingData: self.savingData)
}
func withUpdatedEditingState(_ editingState: ChannelInfoEditingState?) -> ChannelInfoState {
return ChannelInfoState(updatingAvatar: self.updatingAvatar, editingState: editingState, savingData: self.savingData)
}
func withUpdatedSavingData(_ savingData: Bool) -> ChannelInfoState {
return ChannelInfoState(updatingAvatar: self.updatingAvatar, editingState: self.editingState, savingData: savingData)
}
}
private struct ChannelInfoEditingState: Equatable {
let editingName: ItemListAvatarAndNameInfoItemName?
let editingDescriptionText: String
func withUpdatedEditingDescriptionText(_ editingDescriptionText: String) -> ChannelInfoEditingState {
return ChannelInfoEditingState(editingName: self.editingName, editingDescriptionText: editingDescriptionText)
}
static func ==(lhs: ChannelInfoEditingState, rhs: ChannelInfoEditingState) -> Bool {
if lhs.editingName != rhs.editingName {
return false
}
if lhs.editingDescriptionText != rhs.editingDescriptionText {
return false
}
return true
}
}
private func channelInfoEntries(account: Account, presentationData: PresentationData, view: PeerView, globalNotificationSettings: GlobalNotificationSettings, state: ChannelInfoState) -> [ChannelInfoEntry] {
var entries: [ChannelInfoEntry] = []
if let peer = view.peers[view.peerId] as? TelegramChannel {
let canEditChannel = peer.hasPermission(.changeInfo)
let canEditMembers = peer.hasPermission(.banMembers)
let infoState = ItemListAvatarAndNameInfoItemState(editingName: canEditChannel ? state.editingState?.editingName : nil, updatingName: nil)
entries.append(.info(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer: peer, cachedData: view.cachedData, state: infoState, updatingAvatar: state.updatingAvatar))
if let editingState = state.editingState, canEditChannel {
entries.append(.channelPhotoSetup(theme: presentationData.theme, text: presentationData.strings.Channel_UpdatePhotoItem))
entries.append(.channelDescriptionSetup(theme: presentationData.theme, placeholder: presentationData.strings.Channel_About_Placeholder, value: editingState.editingDescriptionText))
}
if let _ = state.editingState, peer.flags.contains(.isCreator) {
let linkText: String
if let username = peer.username {
linkText = "@\(username)"
} else {
linkText = presentationData.strings.Channel_Setup_TypePrivate
}
entries.append(.channelTypeSetup(theme: presentationData.theme, text: presentationData.strings.Channel_TypeSetup_Title, value: linkText))
let discussionGroupTitle: String
if let cachedData = view.cachedData as? CachedChannelData {
if let linkedDiscussionPeerId = cachedData.linkedDiscussionPeerId, let peer = view.peers[linkedDiscussionPeerId] {
if let addressName = peer.addressName, !addressName.isEmpty {
discussionGroupTitle = "@\(addressName)"
} else {
discussionGroupTitle = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
}
} else {
discussionGroupTitle = presentationData.strings.Channel_DiscussionGroupAdd
}
} else {
discussionGroupTitle = "..."
}
entries.append(.discussionGroupSetup(theme: presentationData.theme, text: presentationData.strings.Channel_DiscussionGroup, value: discussionGroupTitle))
entries.append(.discussionGroupSetupInfo(theme: presentationData.theme, text: presentationData.strings.Channel_DiscussionGroupInfo))
let messagesShouldHaveSignatures:Bool
switch peer.info {
case let .broadcast(info):
messagesShouldHaveSignatures = info.flags.contains(.messagesShouldHaveSignatures)
default:
messagesShouldHaveSignatures = false
}
entries.append(.signMessages(theme: presentationData.theme, text: presentationData.strings.Channel_SignMessages, value: messagesShouldHaveSignatures))
entries.append(.signInfo(theme: presentationData.theme, text: presentationData.strings.Channel_SignMessages_Help))
} else {
if state.editingState == nil || !peer.flags.contains(.isCreator) {
if let username = peer.username, !username.isEmpty, state.editingState == nil {
entries.append(.addressName(theme: presentationData.theme, text: presentationData.strings.Channel_LinkItem, value: username))
}
}
if let _ = state.editingState, let adminRights = peer.adminRights, !adminRights.isEmpty {
let discussionGroupTitle: String?
if let cachedData = view.cachedData as? CachedChannelData {
if let linkedDiscussionPeerId = cachedData.linkedDiscussionPeerId, let peer = view.peers[linkedDiscussionPeerId] {
if let addressName = peer.addressName, !addressName.isEmpty {
discussionGroupTitle = "@\(addressName)"
} else {
discussionGroupTitle = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
}
} else if canEditChannel {
discussionGroupTitle = presentationData.strings.Channel_DiscussionGroupAdd
} else {
discussionGroupTitle = nil
}
} else if canEditChannel {
discussionGroupTitle = "..."
} else {
discussionGroupTitle = nil
}
if let discussionGroupTitle = discussionGroupTitle {
entries.append(.discussionGroupSetup(theme: presentationData.theme, text: presentationData.strings.Channel_DiscussionGroup, value: discussionGroupTitle))
if canEditChannel {
entries.append(.discussionGroupSetupInfo(theme: presentationData.theme, text: presentationData.strings.Channel_DiscussionGroupInfo))
}
}
}
}
if let _ = state.editingState {
} else {
if peer.isScam {
entries.append(.about(theme: presentationData.theme, text: presentationData.strings.Channel_AboutItem, value: presentationData.strings.ChannelInfo_ScamChannelWarning))
} else if let cachedChannelData = view.cachedData as? CachedChannelData, let about = cachedChannelData.about, !about.isEmpty {
entries.append(.about(theme: presentationData.theme, text: presentationData.strings.Channel_AboutItem, value: about))
}
}
if let cachedChannelData = view.cachedData as? CachedChannelData {
if canEditMembers {
if peer.adminRights != nil || peer.flags.contains(.isCreator) {
let adminCount = cachedChannelData.participantsSummary.adminCount ?? 0
entries.append(.admins(theme: presentationData.theme, text: presentationData.strings.GroupInfo_Administrators, value: "\(adminCount == 0 ? "" : "\(presentationStringsFormattedNumber(adminCount, presentationData.dateTimeFormat.groupingSeparator))")"))
let memberCount = cachedChannelData.participantsSummary.memberCount ?? 0
entries.append(.members(theme: presentationData.theme, text: presentationData.strings.Channel_Info_Subscribers, value: "\(memberCount == 0 ? "" : "\(presentationStringsFormattedNumber(memberCount, presentationData.dateTimeFormat.groupingSeparator))")"))
let bannedCount = cachedChannelData.participantsSummary.kickedCount ?? 0
entries.append(.banned(theme: presentationData.theme, text: presentationData.strings.GroupRemoved_Title, value: "\(bannedCount == 0 ? "" : "\(presentationStringsFormattedNumber(bannedCount, presentationData.dateTimeFormat.groupingSeparator))")"))
}
}
}
if state.editingState == nil, let notificationSettings = view.notificationSettings as? TelegramPeerNotificationSettings {
let notificationsText: String
if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) {
if until < Int32.max - 1 {
notificationsText = stringForRemainingMuteInterval(strings: presentationData.strings, muteInterval: until)
} else {
notificationsText = presentationData.strings.UserInfo_NotificationsDisabled
}
} else if case .default = notificationSettings.messageSound {
notificationsText = presentationData.strings.UserInfo_NotificationsEnabled
} else {
notificationsText = localizedPeerNotificationSoundString(strings: presentationData.strings, sound: notificationSettings.messageSound, default: globalNotificationSettings.effective.channels.sound)
}
entries.append(ChannelInfoEntry.notifications(theme: presentationData.theme, text: presentationData.strings.GroupInfo_Notifications, value: notificationsText))
}
if state.editingState == nil {
entries.append(ChannelInfoEntry.sharedMedia(theme: presentationData.theme, text: presentationData.strings.GroupInfo_SharedMedia))
if let cachedChannelData = view.cachedData as? CachedChannelData, cachedChannelData.flags.contains(.canViewStats) {
entries.append(ChannelInfoEntry.stats(theme: presentationData.theme, text: presentationData.strings.ChannelInfo_Stats))
}
}
if peer.flags.contains(.isCreator) {
//if state.editingState != nil {
entries.append(ChannelInfoEntry.deleteChannel(theme: presentationData.theme, text: presentationData.strings.ChannelInfo_DeleteChannel))
//}
} else if state.editingState == nil {
entries.append(ChannelInfoEntry.report(theme: presentationData.theme, text: presentationData.strings.ReportPeer_Report))
if peer.participationStatus == .member {
entries.append(ChannelInfoEntry.leave(theme: presentationData.theme, text: presentationData.strings.Channel_LeaveChannel))
}
}
}
return entries
}
private func valuesRequiringUpdate(state: ChannelInfoState, view: PeerView) -> (title: String?, description: String?) {
if let peer = view.peers[view.peerId] as? TelegramChannel {
var titleValue: String?
var descriptionValue: String?
if let editingState = state.editingState {
if let title = editingState.editingName?.composedTitle, title != peer.title {
titleValue = title
}
if let cachedData = view.cachedData as? CachedChannelData {
if let about = cachedData.about {
if about != editingState.editingDescriptionText {
descriptionValue = editingState.editingDescriptionText
}
} else if !editingState.editingDescriptionText.isEmpty {
descriptionValue = editingState.editingDescriptionText
}
}
}
return (titleValue, descriptionValue)
} else {
return (nil, nil)
}
}
public func channelInfoController(context: AccountContext, peerId: PeerId) -> ViewController {
let statePromise = ValuePromise(ChannelInfoState(), ignoreRepeated: true)
let stateValue = Atomic(value: ChannelInfoState())
let updateState: ((ChannelInfoState) -> ChannelInfoState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var pushControllerImpl: ((ViewController) -> Void)?
var presentControllerImpl: ((ViewController, Any?) -> Void)?
var removePeerChatImpl: ((Peer, Bool) -> Void)?
var endEditingImpl: (() -> Void)?
var errorImpl: (() -> Void)?
let actionsDisposable = DisposableSet()
let updatePeerNameDisposable = MetaDisposable()
actionsDisposable.add(updatePeerNameDisposable)
let updatePeerDescriptionDisposable = MetaDisposable()
actionsDisposable.add(updatePeerDescriptionDisposable)
let changeMuteSettingsDisposable = MetaDisposable()
actionsDisposable.add(changeMuteSettingsDisposable)
let hiddenAvatarRepresentationDisposable = MetaDisposable()
actionsDisposable.add(hiddenAvatarRepresentationDisposable)
let updateAvatarDisposable = MetaDisposable()
actionsDisposable.add(updateAvatarDisposable)
let currentAvatarMixin = Atomic<TGMediaAvatarMenuMixin?>(value: nil)
let navigateDisposable = MetaDisposable()
actionsDisposable.add(navigateDisposable)
let statsUrlDisposable = MetaDisposable()
actionsDisposable.add(statsUrlDisposable)
var avatarGalleryTransitionArguments: ((AvatarGalleryEntry) -> GalleryTransitionArguments?)?
let avatarAndNameInfoContext = ItemListAvatarAndNameInfoItemContext()
var updateHiddenAvatarImpl: (() -> Void)?
var displayContextMenuImpl: ((ChannelInfoEntryTag, String) -> Void)?
var aboutLinkActionImpl: ((TextLinkItemActionType, TextLinkItem) -> Void)?
let arguments = ChannelInfoControllerArguments(account: context.account, avatarAndNameInfoContext: avatarAndNameInfoContext, tapAvatarAction: {
let _ = (context.account.postbox.loadedPeerWithId(peerId) |> take(1) |> deliverOnMainQueue).start(next: { peer in
if peer.profileImageRepresentations.isEmpty {
return
}
let galleryController = AvatarGalleryController(context: context, peer: peer, replaceRootController: { controller, ready in
})
hiddenAvatarRepresentationDisposable.set((galleryController.hiddenMedia |> deliverOnMainQueue).start(next: { entry in
avatarAndNameInfoContext.hiddenAvatarRepresentation = entry?.representations.first?.representation
updateHiddenAvatarImpl?()
}))
presentControllerImpl?(galleryController, AvatarGalleryControllerPresentationArguments(transitionArguments: { entry in
return avatarGalleryTransitionArguments?(entry)
}))
})
}, changeProfilePhoto: {
endEditingImpl?()
let _ = (context.account.postbox.transaction { transaction -> (Peer?, SearchBotsConfiguration) in
return (transaction.getPeer(peerId), currentSearchBotsConfiguration(transaction: transaction))
} |> deliverOnMainQueue).start(next: { peer, searchBotsConfiguration in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme)
legacyController.statusBar.statusBarStyle = .Ignore
let emptyController = LegacyEmptyController(context: legacyController.context)!
let navigationController = makeLegacyNavigationController(rootController: emptyController)
navigationController.setNavigationBarHidden(true, animated: false)
navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0)
legacyController.bind(controller: navigationController)
presentControllerImpl?(legacyController, nil)
var hasPhotos = false
if let peer = peer, !peer.profileImageRepresentations.isEmpty {
hasPhotos = true
}
let completedImpl: (UIImage) -> Void = { image in
if let data = image.jpegData(compressionQuality: 0.6) {
let resource = LocalFileMediaResource(fileId: arc4random64())
context.account.postbox.mediaBox.storeResourceData(resource.id, data: data)
let representation = TelegramMediaImageRepresentation(dimensions: CGSize(width: 640.0, height: 640.0), resource: resource)
updateState {
$0.withUpdatedUpdatingAvatar(.image(representation, true))
}
updateAvatarDisposable.set((updatePeerPhoto(postbox: context.account.postbox, network: context.account.network, stateManager: context.account.stateManager, accountPeerId: context.account.peerId, peerId: peerId, photo: uploadedPeerPhoto(postbox: context.account.postbox, network: context.account.network, resource: resource), mapResourceToAvatarSizes: { resource, representations in
return mapResourceToAvatarSizes(postbox: context.account.postbox, resource: resource, representations: representations)
})
|> deliverOnMainQueue).start(next: { result in
switch result {
case .complete:
updateState {
$0.withUpdatedUpdatingAvatar(nil)
}
case .progress:
break
}
}))
}
}
let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasPhotos, hasViewButton: false, personalPhoto: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false)!
let _ = currentAvatarMixin.swap(mixin)
mixin.requestSearchController = { assetsController in
let controller = WebSearchController(context: context, peer: peer, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: peer?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), completion: { result in
assetsController?.dismiss()
completedImpl(result)
}))
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}
mixin.didFinishWithImage = { image in
if let image = image {
completedImpl(image)
}
}
mixin.didFinishWithDelete = {
let _ = currentAvatarMixin.swap(nil)
updateState {
if let profileImage = peer?.smallProfileImage {
return $0.withUpdatedUpdatingAvatar(.image(profileImage, false))
} else {
return $0.withUpdatedUpdatingAvatar(.none)
}
}
updateAvatarDisposable.set((updatePeerPhoto(postbox: context.account.postbox, network: context.account.network, stateManager: context.account.stateManager, accountPeerId: context.account.peerId, peerId: peerId, photo: nil, mapResourceToAvatarSizes: { resource, representations in
return mapResourceToAvatarSizes(postbox: context.account.postbox, resource: resource, representations: representations)
}) |> deliverOnMainQueue).start(next: { result in
switch result {
case .complete:
updateState {
$0.withUpdatedUpdatingAvatar(nil)
}
case .progress:
break
}
}))
}
mixin.didDismiss = { [weak legacyController] in
let _ = currentAvatarMixin.swap(nil)
legacyController?.dismiss()
}
let menuController = mixin.present()
if let menuController = menuController {
menuController.customRemoveFromParentViewController = { [weak legacyController] in
legacyController?.dismiss()
}
}
})
}, updateEditingName: { editingName in
updateState { state in
if let editingState = state.editingState {
return state.withUpdatedEditingState(ChannelInfoEditingState(editingName: editingName, editingDescriptionText: editingState.editingDescriptionText))
} else {
return state
}
}
}, updateEditingDescriptionText: { text in
updateState { state in
if let editingState = state.editingState {
return state.withUpdatedEditingState(editingState.withUpdatedEditingDescriptionText(text))
}
return state
}
}, openChannelTypeSetup: {
presentControllerImpl?(channelVisibilityController(context: context, peerId: peerId, mode: .generic, upgradedToSupergroup: { _, f in f() }), ViewControllerPresentationArguments(presentationAnimation: ViewControllerPresentationAnimation.modalSheet))
}, openDiscussionGroupSetup: {
pushControllerImpl?(channelDiscussionGroupSetupController(context: context, peerId: peerId))
}, changeNotificationMuteSettings: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let _ = (context.account.postbox.transaction { transaction -> (TelegramPeerNotificationSettings, GlobalNotificationSettings) in
let peerSettings: TelegramPeerNotificationSettings = (transaction.getPeerNotificationSettings(peerId) as? TelegramPeerNotificationSettings) ?? TelegramPeerNotificationSettings.defaultSettings
let globalSettings: GlobalNotificationSettings = (transaction.getPreferencesEntry(key: PreferencesKeys.globalNotifications) as? GlobalNotificationSettings) ?? GlobalNotificationSettings.defaultSettings
return (peerSettings, globalSettings)
}
|> deliverOnMainQueue).start(next: { peerSettings, globalSettings in
let soundSettings: NotificationSoundSettings?
if case .default = peerSettings.messageSound {
soundSettings = NotificationSoundSettings(value: nil)
} else {
soundSettings = NotificationSoundSettings(value: peerSettings.messageSound)
}
let controller = notificationMuteSettingsController(presentationData: presentationData, notificationSettings: globalSettings.effective.groupChats, soundSettings: soundSettings, openSoundSettings: {
let controller = notificationSoundSelectionController(context: context, isModal: true, currentSound: peerSettings.messageSound, defaultSound: globalSettings.effective.groupChats.sound, completion: { sound in
let _ = updatePeerNotificationSoundInteractive(account: context.account, peerId: peerId, sound: sound).start()
})
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}, updateSettings: { value in
changeMuteSettingsDisposable.set(updatePeerMuteSetting(account: context.account, peerId: peerId, muteInterval: value).start())
})
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
})
}, openSharedMedia: {
if let controller = context.sharedContext.makePeerSharedMediaController(context: context, peerId: peerId) {
pushControllerImpl?(controller)
}
}, openStats: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var urlSignal = channelStatsUrl(postbox: context.account.postbox, network: context.account.network, peerId: peerId, params: "", darkTheme: presentationData.theme.rootController.keyboardColor.keyboardAppearance == .dark)
var cancelImpl: (() -> Void)?
let progressSignal = Signal<Never, NoError> { subscriber in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
presentControllerImpl?(controller, nil)
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.05, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
urlSignal = urlSignal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
cancelImpl = {
statsUrlDisposable.set(nil)
}
statsUrlDisposable.set((urlSignal
|> deliverOnMainQueue).start(next: { url in
pushControllerImpl?(ChannelStatsController(context: context, url: url, peerId: peerId))
}, 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)
}))
}, openAdmins: {
pushControllerImpl?(channelAdminsController(context: context, peerId: peerId))
}, openMembers: {
pushControllerImpl?(channelMembersController(context: context, peerId: peerId))
}, openBanned: {
pushControllerImpl?(channelBlacklistController(context: context, peerId: peerId))
}, reportChannel: {
presentControllerImpl?(peerReportOptionsController(context: context, subject: .peer(peerId), present: { c, a in
presentControllerImpl?(c, a)
}, completion: { _ in }), nil)
}, leaveChannel: {
let _ = (context.account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(peerId)
}
|> deliverOnMainQueue).start(next: { peer in
guard let peer = peer else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = ActionSheetController(presentationTheme: presentationData.theme)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
controller.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Channel_LeaveChannel, color: .destructive, action: {
dismissAction()
removePeerChatImpl?(peer, false)
}),
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
})
}, deleteChannel: {
let _ = (context.account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(peerId)
}
|> deliverOnMainQueue).start(next: { peer in
guard let peer = peer else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = ActionSheetController(presentationTheme: presentationData.theme)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
controller.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetTextItem(title: presentationData.strings.ChannelInfo_DeleteChannelConfirmation),
ActionSheetButtonItem(title: presentationData.strings.ChannelInfo_DeleteChannel, color: .destructive, action: {
dismissAction()
removePeerChatImpl?(peer, true)
}),
]),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
})
}, displayAddressNameContextMenu: { text in
let shareController = ShareController(context: context, subject: .url(text))
presentControllerImpl?(shareController, nil)
}, displayContextMenu: { tag, text in
displayContextMenuImpl?(tag, text)
}, aboutLinkAction: { action, itemLink in
aboutLinkActionImpl?(action, itemLink)
}, toggleSignatures: { enabled in
actionsDisposable.add(toggleShouldChannelMessagesSignatures(account: context.account, peerId: peerId, enabled: enabled).start())
})
var wasEditing: Bool?
let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set<ValueBoxKey>([PreferencesKeys.globalNotifications]))
let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get(), context.account.viewTracker.peerView(peerId, updateData: true), context.account.postbox.combinedView(keys: [globalNotificationsKey]))
|> map { presentationData, state, view, combinedView -> (ItemListControllerState, (ItemListNodeState, Any)) in
let peer = peerViewMainPeer(view)
var globalNotificationSettings: GlobalNotificationSettings = GlobalNotificationSettings.defaultSettings
if let preferencesView = combinedView.views[globalNotificationsKey] as? PreferencesView {
if let settings = preferencesView.values[PreferencesKeys.globalNotifications] as? GlobalNotificationSettings {
globalNotificationSettings = settings
}
}
var canManageChannel = false
if let peer = peer as? TelegramChannel {
if peer.flags.contains(.isCreator) {
canManageChannel = true
} else if let adminRights = peer.adminRights, !adminRights.isEmpty {
canManageChannel = true
}
}
var canEditChannel = false
var hasSomethingToEdit = false
if let peer = view.peers[view.peerId] as? TelegramChannel {
canEditChannel = peer.hasPermission(.changeInfo)
if canEditChannel {
hasSomethingToEdit = true
} else if let adminRights = peer.adminRights, !adminRights.isEmpty {
if let cachedData = view.cachedData as? CachedChannelData, let _ = cachedData.linkedDiscussionPeerId {
hasSomethingToEdit = true
}
}
}
var leftNavigationButton: ItemListNavigationButton?
var rightNavigationButton: ItemListNavigationButton?
if let editingState = state.editingState {
leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
updateState {
$0.withUpdatedEditingState(nil)
}
})
var doneEnabled = true
if let editingName = editingState.editingName, editingName.isEmpty {
doneEnabled = false
}
if peer is TelegramChannel {
if (view.cachedData as? CachedChannelData) == nil {
doneEnabled = false
}
}
if state.savingData {
rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: doneEnabled, action: {})
} else {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: doneEnabled, action: {
var updateValues: (title: String?, description: String?) = (nil, nil)
var failed = false
updateState { state in
updateValues = valuesRequiringUpdate(state: state, view: view)
if updateValues.0 != nil || updateValues.1 != nil {
if (updateValues.description?.count ?? 0) > 255 {
failed = true
return state
}
return state.withUpdatedSavingData(true)
} else {
return state.withUpdatedEditingState(nil)
}
}
guard !failed else {
errorImpl?()
return
}
let updateTitle: Signal<Void, Void>
if let titleValue = updateValues.title {
updateTitle = updatePeerTitle(account: context.account, peerId: peerId, title: titleValue)
|> mapError { _ in return Void() }
} else {
updateTitle = .complete()
}
let updateDescription: Signal<Void, Void>
if let descriptionValue = updateValues.description {
updateDescription = updatePeerDescription(account: context.account, peerId: peerId, description: descriptionValue.isEmpty ? nil : descriptionValue)
|> mapError { _ in return Void() }
} else {
updateDescription = .complete()
}
let signal = combineLatest(updateTitle, updateDescription)
updatePeerNameDisposable.set((signal |> deliverOnMainQueue).start(error: { _ in
updateState { state in
return state.withUpdatedSavingData(false)
}
}, completed: {
updateState { state in
return state.withUpdatedSavingData(false).withUpdatedEditingState(nil)
}
}))
})
}
} else if hasSomethingToEdit {
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: {
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
var text = ""
if let cachedData = view.cachedData as? CachedChannelData, let about = cachedData.about {
text = about
}
updateState { state in
return state.withUpdatedEditingState(ChannelInfoEditingState(editingName: ItemListAvatarAndNameInfoItemName(channel), editingDescriptionText: text))
}
}
})
}
let wasEditingValue = wasEditing
wasEditing = state.editingState != nil
var crossfadeState = false
if let wasEditingValue = wasEditingValue, wasEditingValue != (state.editingState != nil) {
crossfadeState = true
}
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.UserInfo_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
let listState = ItemListNodeState(entries: channelInfoEntries(account: context.account, presentationData: presentationData, view: view, globalNotificationSettings: globalNotificationSettings, state: state), style: .plain, crossfadeState: crossfadeState, animateChanges: false)
return (controllerState, (listState, arguments))
} |> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
pushControllerImpl = { [weak controller] value in
(controller?.navigationController as? NavigationController)?.pushViewController(value)
}
presentControllerImpl = { [weak controller] value, presentationArguments in
controller?.present(value, in: .window(.root), with: presentationArguments, blockInteraction: true)
}
removePeerChatImpl = { [weak controller] peer, deleteGloballyIfPossible in
guard let controller = controller, let navigationController = controller.navigationController as? NavigationController else {
return
}
guard let tabController = navigationController.viewControllers.first as? TabBarController else {
return
}
for childController in tabController.controllers {
if let chatListController = childController as? ChatListController {
chatListController.maybeAskForPeerChatRemoval(peer: RenderedPeer(peer: peer), deleteGloballyIfPossible: deleteGloballyIfPossible, completion: { [weak navigationController] deleted in
if deleted {
navigationController?.popToRoot(animated: true)
}
}, removed: {
})
break
}
}
}
avatarGalleryTransitionArguments = { [weak controller] entry in
if let controller = controller {
var result: ((ASDisplayNode, () -> (UIView?, UIView?)), CGRect)?
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListAvatarAndNameInfoItemNode {
result = itemNode.avatarTransitionNode()
}
}
if let (node, _) = result {
return GalleryTransitionArguments(transitionNode: node, addToTransitionSurface: { _ in
})
}
}
return nil
}
updateHiddenAvatarImpl = { [weak controller] in
if let controller = controller {
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListAvatarAndNameInfoItemNode {
itemNode.updateAvatarHidden()
}
}
}
}
displayContextMenuImpl = { [weak controller] tag, text in
if let strongController = controller {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var resultItemNode: ListViewItemNode?
let _ = strongController.frameForItemNode({ itemNode in
if let itemNode = itemNode as? ItemListTextWithLabelItemNode {
if let itemTag = itemNode.tag as? ChannelInfoEntryTag {
if itemTag == tag {
resultItemNode = itemNode
return true
}
}
}
return false
})
if let resultItemNode = resultItemNode {
let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(title: presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: presentationData.strings.Conversation_ContextMenuCopy), action: {
UIPasteboard.general.string = text
})])
strongController.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in
if let strongController = controller, let resultItemNode = resultItemNode {
return (resultItemNode, resultItemNode.contentBounds.insetBy(dx: 0.0, dy: -2.0), strongController.displayNode, strongController.view.bounds)
} else {
return nil
}
}))
}
}
}
aboutLinkActionImpl = { [weak context, weak controller] action, itemLink in
if let controller = controller, let context = context {
context.sharedContext.handleTextLinkAction(context: context, peerId: peerId, navigateDisposable: navigateDisposable, controller: controller, action: action, itemLink: itemLink)
}
}
endEditingImpl = {
[weak controller] in
controller?.view.endEditing(true)
}
let hapticFeedback = HapticFeedback()
errorImpl = { [weak controller] in
hapticFeedback.error()
controller?.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListMultilineInputItemNode {
itemNode.animateError()
}
}
}
return controller
}