mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-02 00:17:02 +00:00
Added recent stickers clearing Added sending logs via email Added forward recipient change on forward acccessory panel tap Tweaked undo panel design Various UI fixes
2343 lines
124 KiB
Swift
2343 lines
124 KiB
Swift
import Foundation
|
|
import Display
|
|
import SwiftSignalKit
|
|
import Postbox
|
|
import TelegramCore
|
|
import LegacyComponents
|
|
|
|
import SafariServices
|
|
|
|
private final class GroupInfoArguments {
|
|
let context: AccountContext
|
|
|
|
let avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext
|
|
let tapAvatarAction: () -> Void
|
|
let changeProfilePhoto: () -> Void
|
|
let pushController: (ViewController) -> Void
|
|
let presentController: (ViewController, ViewControllerPresentationArguments) -> Void
|
|
let changeNotificationMuteSettings: () -> Void
|
|
let openPreHistory: () -> Void
|
|
let openSharedMedia: () -> Void
|
|
let openAdministrators: () -> Void
|
|
let openPermissions: () -> Void
|
|
let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void
|
|
let updateEditingDescriptionText: (String) -> Void
|
|
let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void
|
|
let addMember: () -> Void
|
|
let promotePeer: (RenderedChannelParticipant) -> Void
|
|
let restrictPeer: (RenderedChannelParticipant) -> Void
|
|
let removePeer: (PeerId) -> Void
|
|
let leave: () -> Void
|
|
let displayUsernameShareMenu: (String) -> Void
|
|
let displayUsernameContextMenu: (String) -> Void
|
|
let displayAboutContextMenu: (String) -> Void
|
|
let aboutLinkAction: (TextLinkItemActionType, TextLinkItem) -> Void
|
|
let openStickerPackSetup: () -> Void
|
|
let openGroupTypeSetup: () -> Void
|
|
|
|
init(context: AccountContext, avatarAndNameInfoContext: ItemListAvatarAndNameInfoItemContext, tapAvatarAction: @escaping () -> Void, changeProfilePhoto: @escaping () -> Void, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController, ViewControllerPresentationArguments) -> Void, changeNotificationMuteSettings: @escaping () -> Void, openPreHistory: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openAdministrators: @escaping () -> Void, openPermissions: @escaping () -> Void, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updateEditingDescriptionText: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addMember: @escaping () -> Void, promotePeer: @escaping (RenderedChannelParticipant) -> Void, restrictPeer: @escaping (RenderedChannelParticipant) -> Void, removePeer: @escaping (PeerId) -> Void, leave: @escaping () -> Void, displayUsernameShareMenu: @escaping (String) -> Void, displayUsernameContextMenu: @escaping (String) -> Void, displayAboutContextMenu: @escaping (String) -> Void, aboutLinkAction: @escaping (TextLinkItemActionType, TextLinkItem) -> Void, openStickerPackSetup: @escaping () -> Void, openGroupTypeSetup: @escaping () -> Void) {
|
|
self.context = context
|
|
self.avatarAndNameInfoContext = avatarAndNameInfoContext
|
|
self.tapAvatarAction = tapAvatarAction
|
|
self.changeProfilePhoto = changeProfilePhoto
|
|
self.pushController = pushController
|
|
self.presentController = presentController
|
|
self.changeNotificationMuteSettings = changeNotificationMuteSettings
|
|
self.openPreHistory = openPreHistory
|
|
self.openSharedMedia = openSharedMedia
|
|
self.openAdministrators = openAdministrators
|
|
self.openPermissions = openPermissions
|
|
self.updateEditingName = updateEditingName
|
|
self.updateEditingDescriptionText = updateEditingDescriptionText
|
|
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
|
|
self.addMember = addMember
|
|
self.promotePeer = promotePeer
|
|
self.restrictPeer = restrictPeer
|
|
self.removePeer = removePeer
|
|
self.leave = leave
|
|
self.displayUsernameShareMenu = displayUsernameShareMenu
|
|
self.displayUsernameContextMenu = displayUsernameContextMenu
|
|
self.displayAboutContextMenu = displayAboutContextMenu
|
|
self.aboutLinkAction = aboutLinkAction
|
|
self.openStickerPackSetup = openStickerPackSetup
|
|
self.openGroupTypeSetup = openGroupTypeSetup
|
|
}
|
|
}
|
|
|
|
private enum GroupInfoSection: ItemListSectionId {
|
|
case info
|
|
case about
|
|
case infoManagement
|
|
case sharedMediaAndNotifications
|
|
case memberManagement
|
|
case members
|
|
case leave
|
|
}
|
|
|
|
private enum GroupInfoEntryTag {
|
|
case about
|
|
case link
|
|
}
|
|
|
|
private enum GroupInfoMemberStatus {
|
|
case member
|
|
case admin
|
|
}
|
|
|
|
private enum GroupEntryStableId: Hashable, Equatable {
|
|
case peer(PeerId)
|
|
case index(Int)
|
|
|
|
static func ==(lhs: GroupEntryStableId, rhs: GroupEntryStableId) -> Bool {
|
|
switch lhs {
|
|
case let .peer(peerId):
|
|
if case .peer(peerId) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .index(index):
|
|
if case .index(index) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
var hashValue: Int {
|
|
switch self {
|
|
case let .peer(peerId):
|
|
return peerId.hashValue
|
|
case let .index(index):
|
|
return index.hashValue
|
|
}
|
|
}
|
|
}
|
|
|
|
enum ParticipantRevealActionType {
|
|
case promote
|
|
case restrict
|
|
case remove
|
|
}
|
|
|
|
struct ParticipantRevealAction: Equatable {
|
|
let type: ItemListPeerItemRevealOptionType
|
|
let title: String
|
|
let action: ParticipantRevealActionType
|
|
}
|
|
|
|
private enum GroupInfoEntry: ItemListNodeEntry {
|
|
case info(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, peer: Peer?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState, updatingAvatar: ItemListAvatarAndNameInfoItemUpdatingAvatar?)
|
|
case setGroupPhoto(PresentationTheme, String)
|
|
case groupDescriptionSetup(PresentationTheme, String, String)
|
|
case aboutHeader(PresentationTheme, String)
|
|
case about(PresentationTheme, String)
|
|
case link(PresentationTheme, String)
|
|
case sharedMedia(PresentationTheme, String)
|
|
case notifications(PresentationTheme, String, String)
|
|
case stickerPack(PresentationTheme, String, String)
|
|
case groupTypeSetup(PresentationTheme, String, String)
|
|
case preHistory(PresentationTheme, String, String)
|
|
case administrators(PresentationTheme, String, String)
|
|
case permissions(PresentationTheme, String, String)
|
|
case addMember(PresentationTheme, String, editing: Bool)
|
|
case member(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, index: Int, peerId: PeerId, peer: Peer, participant: RenderedChannelParticipant?, presence: PeerPresence?, memberStatus: GroupInfoMemberStatus, editing: ItemListPeerItemEditing, revealActions: [ParticipantRevealAction], enabled: Bool)
|
|
case leave(PresentationTheme, String)
|
|
|
|
var section: ItemListSectionId {
|
|
switch self {
|
|
case .info, .setGroupPhoto, .groupDescriptionSetup:
|
|
return GroupInfoSection.info.rawValue
|
|
case .aboutHeader, .about, .link:
|
|
return GroupInfoSection.about.rawValue
|
|
case .groupTypeSetup, .preHistory, .stickerPack:
|
|
return GroupInfoSection.infoManagement.rawValue
|
|
case .sharedMedia, .notifications:
|
|
return GroupInfoSection.sharedMediaAndNotifications.rawValue
|
|
case .permissions, .administrators:
|
|
return GroupInfoSection.memberManagement.rawValue
|
|
case .addMember, .member:
|
|
return GroupInfoSection.members.rawValue
|
|
case .leave:
|
|
return GroupInfoSection.leave.rawValue
|
|
}
|
|
}
|
|
|
|
static func ==(lhs: GroupInfoEntry, rhs: GroupInfoEntry) -> 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 .setGroupPhoto(lhsTheme, lhsText):
|
|
if case let .setGroupPhoto(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .groupDescriptionSetup(lhsTheme, lhsPlaceholder, lhsText):
|
|
if case let .groupDescriptionSetup(rhsTheme, rhsPlaceholder, rhsText) = rhs, lhsTheme === rhsTheme, lhsPlaceholder == rhsPlaceholder, 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 .leave(lhsTheme, lhsText):
|
|
if case let .leave(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .aboutHeader(lhsTheme, lhsText):
|
|
if case let .aboutHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .about(lhsTheme, lhsText):
|
|
if case let .about(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .link(lhsTheme, lhsText):
|
|
if case let .link(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .notifications(lhsTheme, lhsTitle, lhsText):
|
|
if case let .notifications(rhsTheme, rhsTitle, rhsText) = rhs {
|
|
if lhsTheme !== rhsTheme {
|
|
return false
|
|
}
|
|
if lhsTitle != rhsTitle {
|
|
return false
|
|
}
|
|
if lhsText != rhsText {
|
|
return false
|
|
}
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .stickerPack(lhsTheme, lhsTitle, lhsValue):
|
|
if case let .stickerPack(rhsTheme, rhsTitle, rhsValue) = rhs {
|
|
if lhsTheme !== rhsTheme {
|
|
return false
|
|
}
|
|
if lhsTitle != rhsTitle {
|
|
return false
|
|
}
|
|
if lhsValue != rhsValue {
|
|
return false
|
|
}
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .preHistory(lhsTheme, lhsTitle, lhsValue):
|
|
if case let .preHistory(rhsTheme, rhsTitle, rhsValue) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .groupTypeSetup(lhsTheme, lhsTitle, lhsText):
|
|
if case let .groupTypeSetup(rhsTheme, rhsTitle, rhsText) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsText == rhsText {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .permissions(lhsTheme, lhsTitle, lhsText):
|
|
if case let .permissions(rhsTheme, rhsTitle, rhsText) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsText == rhsText {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .administrators(lhsTheme, lhsTitle, lhsText):
|
|
if case let .administrators(rhsTheme, rhsTitle, rhsText) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsText == rhsText {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .addMember(lhsTheme, lhsTitle, lhsEditing):
|
|
if case let .addMember(rhsTheme, rhsTitle, rhsEditing) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsEditing == rhsEditing {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .member(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsNameDisplayOrder, lhsIndex, lhsPeerId, lhsPeer, lhsParticipant, lhsPresence, lhsMemberStatus, lhsEditing, lhsActions, lhsEnabled):
|
|
if case let .member(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsNameDisplayOrder, rhsIndex, rhsPeerId, rhsPeer, rhsParticipant, rhsPresence, rhsMemberStatus, rhsEditing, rhsActions, rhsEnabled) = rhs {
|
|
if lhsTheme !== rhsTheme {
|
|
return false
|
|
}
|
|
if lhsStrings !== rhsStrings {
|
|
return false
|
|
}
|
|
if lhsDateTimeFormat != rhsDateTimeFormat {
|
|
return false
|
|
}
|
|
if lhsNameDisplayOrder != rhsNameDisplayOrder {
|
|
return false
|
|
}
|
|
if lhsIndex != rhsIndex {
|
|
return false
|
|
}
|
|
if lhsMemberStatus != rhsMemberStatus {
|
|
return false
|
|
}
|
|
if lhsPeerId != rhsPeerId {
|
|
return false
|
|
}
|
|
if !lhsPeer.isEqual(rhsPeer) {
|
|
return false
|
|
}
|
|
if lhsParticipant != rhsParticipant {
|
|
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 lhsEditing != rhsEditing {
|
|
return false
|
|
}
|
|
if lhsActions != rhsActions {
|
|
return false
|
|
}
|
|
if lhsEnabled != rhsEnabled {
|
|
return false
|
|
}
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
var stableId: GroupEntryStableId {
|
|
switch self {
|
|
case let .member(_, _, _, _, _, peerId, _, _, _, _, _, _, _):
|
|
return .peer(peerId)
|
|
default:
|
|
return .index(self.sortIndex)
|
|
}
|
|
}
|
|
|
|
private var sortIndex: Int {
|
|
switch self {
|
|
case .info:
|
|
return 0
|
|
case .setGroupPhoto:
|
|
return 1
|
|
case .groupDescriptionSetup:
|
|
return 2
|
|
case .aboutHeader:
|
|
return 4
|
|
case .about:
|
|
return 5
|
|
case .link:
|
|
return 6
|
|
case .groupTypeSetup:
|
|
return 8
|
|
case .preHistory:
|
|
return 9
|
|
case .stickerPack:
|
|
return 10
|
|
case .notifications:
|
|
return 11
|
|
case .sharedMedia:
|
|
return 13
|
|
case .permissions:
|
|
return 14
|
|
case .administrators:
|
|
return 15
|
|
case .addMember:
|
|
return 17
|
|
case let .member(_, _, _, _, index, _, _, _, _, _, _, _, _):
|
|
return 20 + index
|
|
case .leave:
|
|
return 100000 + 1
|
|
}
|
|
}
|
|
|
|
static func <(lhs: GroupInfoEntry, rhs: GroupInfoEntry) -> Bool {
|
|
return lhs.sortIndex < rhs.sortIndex
|
|
}
|
|
|
|
func item(_ arguments: GroupInfoArguments) -> ListViewItem {
|
|
switch self {
|
|
case let .info(theme, strings, dateTimeFormat, peer, cachedData, state, updatingAvatar):
|
|
return ItemListAvatarAndNameInfoItem(account: arguments.context.account, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, mode: .generic, peer: peer, presence: nil, cachedData: cachedData, state: state, sectionId: self.section, style: .blocks(withTopInset: false), editingNameUpdated: { editingName in
|
|
arguments.updateEditingName(editingName)
|
|
}, avatarTapped: {
|
|
arguments.tapAvatarAction()
|
|
}, context: arguments.avatarAndNameInfoContext, updatingImage: updatingAvatar)
|
|
case let .setGroupPhoto(theme, text):
|
|
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
|
|
arguments.changeProfilePhoto()
|
|
})
|
|
case let .aboutHeader(theme, text):
|
|
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
|
|
case let .about(theme, text):
|
|
return ItemListMultilineTextItem(theme: theme, text: text, enabledEntitiyTypes: [.url, .mention, .hashtag], sectionId: self.section, style: .blocks, longTapAction: {
|
|
arguments.displayAboutContextMenu(text)
|
|
}, linkItemAction: { action, itemLink in
|
|
arguments.aboutLinkAction(action, itemLink)
|
|
}, tag: GroupInfoEntryTag.about)
|
|
case let .link(theme, url):
|
|
return ItemListActionItem(theme: theme, title: url, kind: .neutral, alignment: .natural, sectionId: self.section, style: .blocks, action: {
|
|
arguments.displayUsernameShareMenu(url)
|
|
}, longTapAction: {
|
|
arguments.displayUsernameContextMenu(url)
|
|
}, tag: GroupInfoEntryTag.link)
|
|
case let .notifications(theme, title, text):
|
|
return ItemListDisclosureItem(theme: theme, title: title, label: text, sectionId: self.section, style: .blocks, action: {
|
|
arguments.changeNotificationMuteSettings()
|
|
})
|
|
case let .stickerPack(theme, title, value):
|
|
return ItemListDisclosureItem(theme: theme, title: title, label: value, sectionId: self.section, style: .blocks, action: {
|
|
arguments.openStickerPackSetup()
|
|
})
|
|
case let .preHistory(theme, title, value):
|
|
return ItemListDisclosureItem(theme: theme, title: title, label: value, sectionId: self.section, style: .blocks, action: {
|
|
arguments.openPreHistory()
|
|
})
|
|
case let .sharedMedia(theme, title):
|
|
return ItemListDisclosureItem(theme: theme, title: title, label: "", sectionId: self.section, style: .blocks, action: {
|
|
arguments.openSharedMedia()
|
|
})
|
|
case let .addMember(theme, title, editing):
|
|
return ItemListPeerActionItem(theme: theme, icon: PresentationResourcesItemList.addPersonIcon(theme), title: title, sectionId: self.section, editing: editing, action: {
|
|
arguments.addMember()
|
|
})
|
|
case let .groupTypeSetup(theme, title, text):
|
|
return ItemListDisclosureItem(theme: theme, title: title, label: text, sectionId: self.section, style: .blocks, action: {
|
|
arguments.openGroupTypeSetup()
|
|
})
|
|
case let .groupDescriptionSetup(theme, placeholder, text):
|
|
return ItemListMultilineInputItem(theme: theme, text: text, placeholder: placeholder, maxLength: ItemListMultilineInputItemTextLimit(value: 255, display: true), sectionId: self.section, style: .blocks, textUpdated: { updatedText in
|
|
arguments.updateEditingDescriptionText(updatedText)
|
|
}, action: {
|
|
|
|
})
|
|
case let .permissions(theme, title, text):
|
|
return ItemListDisclosureItem(theme: theme, icon: PresentationResourcesChat.groupInfoPermissionsIcon(theme), title: title, label: text, sectionId: self.section, style: .blocks, action: {
|
|
arguments.openPermissions()
|
|
})
|
|
case let .administrators(theme, title, text):
|
|
return ItemListDisclosureItem(theme: theme, icon: PresentationResourcesChat.groupInfoAdminsIcon(theme), title: title, label: text, sectionId: self.section, style: .blocks, action: {
|
|
arguments.openAdministrators()
|
|
})
|
|
case let .member(theme, strings, dateTimeFormat, nameDisplayOrder, _, _, peer, participant, presence, memberStatus, editing, actions, enabled):
|
|
let label: String?
|
|
switch memberStatus {
|
|
case .admin:
|
|
label = strings.GroupInfo_LabelAdmin
|
|
case .member:
|
|
label = nil
|
|
}
|
|
var options: [ItemListPeerItemRevealOption] = []
|
|
for action in actions {
|
|
options.append(ItemListPeerItemRevealOption(type: action.type, title: action.title, action: {
|
|
switch action.action {
|
|
case .promote:
|
|
if let participant = participant {
|
|
arguments.promotePeer(participant)
|
|
}
|
|
case .restrict:
|
|
if let participant = participant {
|
|
arguments.restrictPeer(participant)
|
|
}
|
|
case .remove:
|
|
arguments.removePeer(peer.id)
|
|
}
|
|
}))
|
|
}
|
|
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.context.account, peer: peer, presence: presence, text: .presence, label: label == nil ? .none : .text(label!), editing: editing, revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: enabled, sectionId: self.section, action: {
|
|
if let infoController = peerInfoController(context: arguments.context, peer: peer) {
|
|
arguments.pushController(infoController)
|
|
}
|
|
}, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
|
|
arguments.setPeerIdWithRevealedOptions(peerId, fromPeerId)
|
|
}, removePeer: { peerId in
|
|
arguments.removePeer(peerId)
|
|
})
|
|
case let .leave(theme, title):
|
|
return ItemListActionItem(theme: theme, title: title, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: {
|
|
arguments.leave()
|
|
})
|
|
default:
|
|
preconditionFailure()
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct TemporaryParticipant: Equatable {
|
|
let peer: Peer
|
|
let presence: PeerPresence?
|
|
let timestamp: Int32
|
|
|
|
static func ==(lhs: TemporaryParticipant, rhs: TemporaryParticipant) -> Bool {
|
|
if !lhs.peer.isEqual(rhs.peer) {
|
|
return false
|
|
}
|
|
if let lhsPresence = lhs.presence, let rhsPresence = rhs.presence {
|
|
if !lhsPresence.isEqual(to: rhsPresence) {
|
|
return false
|
|
}
|
|
} else if (lhs.presence != nil) != (rhs.presence != nil) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
private struct GroupInfoState: Equatable {
|
|
let updatingAvatar: ItemListAvatarAndNameInfoItemUpdatingAvatar?
|
|
let editingState: GroupInfoEditingState?
|
|
let updatingName: ItemListAvatarAndNameInfoItemName?
|
|
let peerIdWithRevealedOptions: PeerId?
|
|
|
|
let temporaryParticipants: [TemporaryParticipant]
|
|
let successfullyAddedParticipantIds: Set<PeerId>
|
|
let removingParticipantIds: Set<PeerId>
|
|
|
|
let savingData: Bool
|
|
|
|
let searchingMembers: Bool
|
|
|
|
static func ==(lhs: GroupInfoState, rhs: GroupInfoState) -> Bool {
|
|
if lhs.updatingAvatar != rhs.updatingAvatar {
|
|
return false
|
|
}
|
|
if lhs.editingState != rhs.editingState {
|
|
return false
|
|
}
|
|
if lhs.updatingName != rhs.updatingName {
|
|
return false
|
|
}
|
|
if lhs.peerIdWithRevealedOptions != rhs.peerIdWithRevealedOptions {
|
|
return false
|
|
}
|
|
if lhs.temporaryParticipants != rhs.temporaryParticipants {
|
|
return false
|
|
}
|
|
if lhs.successfullyAddedParticipantIds != rhs.successfullyAddedParticipantIds {
|
|
return false
|
|
}
|
|
if lhs.removingParticipantIds != rhs.removingParticipantIds {
|
|
return false
|
|
}
|
|
if lhs.savingData != rhs.savingData {
|
|
return false
|
|
}
|
|
if lhs.searchingMembers != rhs.searchingMembers {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func withUpdatedUpdatingAvatar(_ updatingAvatar: ItemListAvatarAndNameInfoItemUpdatingAvatar?) -> GroupInfoState {
|
|
return GroupInfoState(updatingAvatar: updatingAvatar, editingState: self.editingState, updatingName: self.updatingName, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData, searchingMembers: self.searchingMembers)
|
|
}
|
|
|
|
func withUpdatedEditingState(_ editingState: GroupInfoEditingState?) -> GroupInfoState {
|
|
return GroupInfoState(updatingAvatar: self.updatingAvatar, editingState: editingState, updatingName: self.updatingName, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData, searchingMembers: self.searchingMembers)
|
|
}
|
|
|
|
func withUpdatedUpdatingName(_ updatingName: ItemListAvatarAndNameInfoItemName?) -> GroupInfoState {
|
|
return GroupInfoState(updatingAvatar: self.updatingAvatar, editingState: self.editingState, updatingName: updatingName, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData, searchingMembers: self.searchingMembers)
|
|
}
|
|
|
|
func withUpdatedPeerIdWithRevealedOptions(_ peerIdWithRevealedOptions: PeerId?) -> GroupInfoState {
|
|
return GroupInfoState(updatingAvatar: self.updatingAvatar, editingState: self.editingState, updatingName: self.updatingName, peerIdWithRevealedOptions: peerIdWithRevealedOptions, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData, searchingMembers: self.searchingMembers)
|
|
}
|
|
|
|
func withUpdatedTemporaryParticipants(_ temporaryParticipants: [TemporaryParticipant]) -> GroupInfoState {
|
|
return GroupInfoState(updatingAvatar: self.updatingAvatar, editingState: self.editingState, updatingName: self.updatingName, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, temporaryParticipants: temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData, searchingMembers: self.searchingMembers)
|
|
}
|
|
|
|
func withUpdatedSuccessfullyAddedParticipantIds(_ successfullyAddedParticipantIds: Set<PeerId>) -> GroupInfoState {
|
|
return GroupInfoState(updatingAvatar: self.updatingAvatar, editingState: self.editingState, updatingName: self.updatingName, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData, searchingMembers: self.searchingMembers)
|
|
}
|
|
|
|
func withUpdatedRemovingParticipantIds(_ removingParticipantIds: Set<PeerId>) -> GroupInfoState {
|
|
return GroupInfoState(updatingAvatar: self.updatingAvatar, editingState: self.editingState, updatingName: self.updatingName, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: removingParticipantIds, savingData: self.savingData, searchingMembers: self.searchingMembers)
|
|
}
|
|
|
|
func withUpdatedSavingData(_ savingData: Bool) -> GroupInfoState {
|
|
return GroupInfoState(updatingAvatar: self.updatingAvatar, editingState: self.editingState, updatingName: self.updatingName, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: savingData, searchingMembers: self.searchingMembers)
|
|
}
|
|
|
|
func withUpdatedSearchingMembers(_ searchingMembers: Bool) -> GroupInfoState {
|
|
return GroupInfoState(updatingAvatar: self.updatingAvatar, editingState: self.editingState, updatingName: self.updatingName, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData, searchingMembers: searchingMembers)
|
|
}
|
|
}
|
|
|
|
private struct GroupInfoEditingState: Equatable {
|
|
let editingName: ItemListAvatarAndNameInfoItemName?
|
|
let editingDescriptionText: String
|
|
|
|
func withUpdatedEditingDescriptionText(_ editingDescriptionText: String) -> GroupInfoEditingState {
|
|
return GroupInfoEditingState(editingName: self.editingName, editingDescriptionText: editingDescriptionText)
|
|
}
|
|
|
|
static func ==(lhs: GroupInfoEditingState, rhs: GroupInfoEditingState) -> Bool {
|
|
if lhs.editingName != rhs.editingName {
|
|
return false
|
|
}
|
|
if lhs.editingDescriptionText != rhs.editingDescriptionText {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
private func canRemoveParticipant(account: Account, isAdmin: Bool, participantId: PeerId, invitedBy: PeerId?) -> Bool {
|
|
if participantId == account.peerId {
|
|
return false
|
|
}
|
|
|
|
if account.peerId == invitedBy {
|
|
return true
|
|
}
|
|
|
|
return isAdmin
|
|
}
|
|
|
|
private func canRemoveParticipant(account: Account, channel: TelegramChannel, participant: ChannelParticipant) -> Bool {
|
|
if participant.peerId == account.peerId {
|
|
return false
|
|
}
|
|
|
|
if channel.flags.contains(.isCreator) {
|
|
return true
|
|
}
|
|
|
|
switch participant {
|
|
case .creator:
|
|
return false
|
|
case let .member(_, _, adminInfo, _):
|
|
if channel.hasPermission(.banMembers) {
|
|
if let adminInfo = adminInfo {
|
|
return adminInfo.promotedBy == account.peerId
|
|
} else {
|
|
return false
|
|
}
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private func groupInfoEntries(account: Account, presentationData: PresentationData, view: PeerView, channelMembers: [RenderedChannelParticipant], globalNotificationSettings: GlobalNotificationSettings, state: GroupInfoState) -> [GroupInfoEntry] {
|
|
var entries: [GroupInfoEntry] = []
|
|
|
|
var canEditGroupInfo = false
|
|
var canEditMembers = false
|
|
var canAddMembers = false
|
|
var isPublic = false
|
|
var isCreator = false
|
|
if let group = view.peers[view.peerId] as? TelegramGroup {
|
|
if case .creator = group.role {
|
|
isCreator = true
|
|
}
|
|
switch group.role {
|
|
case .admin, .creator:
|
|
canEditGroupInfo = true
|
|
canEditMembers = true
|
|
canAddMembers = true
|
|
case .member:
|
|
break
|
|
}
|
|
if !group.hasBannedPermission(.banChangeInfo) {
|
|
canEditGroupInfo = true
|
|
}
|
|
if !group.hasBannedPermission(.banAddMembers) {
|
|
canAddMembers = true
|
|
}
|
|
} else if let channel = view.peers[view.peerId] as? TelegramChannel {
|
|
isPublic = channel.username != nil
|
|
isCreator = channel.flags.contains(.isCreator)
|
|
if channel.hasPermission(.changeInfo) {
|
|
canEditGroupInfo = true
|
|
}
|
|
if channel.hasPermission(.banMembers) {
|
|
canEditMembers = true
|
|
}
|
|
if channel.hasPermission(.inviteMembers) {
|
|
canAddMembers = true
|
|
}
|
|
}
|
|
|
|
if let peer = peerViewMainPeer(view) {
|
|
let infoState = ItemListAvatarAndNameInfoItemState(editingName: canEditGroupInfo ? state.editingState?.editingName : nil, updatingName: state.updatingName)
|
|
entries.append(.info(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer: peer, cachedData: view.cachedData, state: infoState, updatingAvatar: state.updatingAvatar))
|
|
}
|
|
|
|
let peerNotificationSettings: TelegramPeerNotificationSettings = (view.notificationSettings as? TelegramPeerNotificationSettings) ?? TelegramPeerNotificationSettings.defaultSettings
|
|
let notificationsText: String
|
|
|
|
if case let .muted(until) = peerNotificationSettings.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 = peerNotificationSettings.messageSound {
|
|
notificationsText = presentationData.strings.UserInfo_NotificationsEnabled
|
|
} else {
|
|
notificationsText = localizedPeerNotificationSoundString(strings: presentationData.strings, sound: peerNotificationSettings.messageSound, default: globalNotificationSettings.effective.channels.sound)
|
|
}
|
|
|
|
if let editingState = state.editingState {
|
|
if canEditGroupInfo {
|
|
entries.append(GroupInfoEntry.setGroupPhoto(presentationData.theme, presentationData.strings.GroupInfo_SetGroupPhoto))
|
|
|
|
entries.append(GroupInfoEntry.groupDescriptionSetup(presentationData.theme, presentationData.strings.Channel_About_Placeholder, editingState.editingDescriptionText))
|
|
}
|
|
|
|
if let group = view.peers[view.peerId] as? TelegramGroup, let cachedGroupData = view.cachedData as? CachedGroupData {
|
|
if case .creator = group.role {
|
|
if cachedGroupData.flags.contains(.canChangeUsername) {
|
|
entries.append(GroupInfoEntry.groupTypeSetup(presentationData.theme, presentationData.strings.GroupInfo_GroupType, presentationData.strings.Channel_Setup_TypePrivate))
|
|
}
|
|
entries.append(GroupInfoEntry.preHistory(presentationData.theme, presentationData.strings.GroupInfo_GroupHistory, presentationData.strings.GroupInfo_GroupHistoryHidden))
|
|
|
|
var activePermissionCount: Int?
|
|
if let defaultBannedRights = group.defaultBannedRights {
|
|
var count = 0
|
|
for right in allGroupPermissionList {
|
|
if !defaultBannedRights.flags.contains(right) {
|
|
count += 1
|
|
}
|
|
}
|
|
activePermissionCount = count
|
|
}
|
|
entries.append(GroupInfoEntry.permissions(presentationData.theme, presentationData.strings.GroupInfo_Permissions, activePermissionCount.flatMap({ "\($0)/\(allGroupPermissionList.count)" }) ?? ""))
|
|
entries.append(.administrators(presentationData.theme, presentationData.strings.GroupInfo_Administrators, ""))
|
|
}
|
|
} else if let channel = view.peers[view.peerId] as? TelegramChannel, let cachedChannelData = view.cachedData as? CachedChannelData {
|
|
if isCreator {
|
|
if cachedChannelData.flags.contains(.canChangeUsername) {
|
|
entries.append(GroupInfoEntry.groupTypeSetup(presentationData.theme, presentationData.strings.GroupInfo_GroupType, isPublic ? presentationData.strings.Channel_Setup_TypePublic : presentationData.strings.Channel_Setup_TypePrivate))
|
|
}
|
|
if !isPublic {
|
|
entries.append(GroupInfoEntry.preHistory(presentationData.theme, presentationData.strings.GroupInfo_GroupHistory, cachedChannelData.flags.contains(.preHistoryEnabled) ? presentationData.strings.GroupInfo_GroupHistoryVisible : presentationData.strings.GroupInfo_GroupHistoryHidden))
|
|
}
|
|
}
|
|
|
|
if cachedChannelData.flags.contains(.canSetStickerSet) && canEditGroupInfo {
|
|
entries.append(GroupInfoEntry.stickerPack(presentationData.theme, presentationData.strings.Stickers_GroupStickers, cachedChannelData.stickerPack?.title ?? presentationData.strings.GroupInfo_SharedMediaNone))
|
|
}
|
|
|
|
var canViewAdminsAndBanned = false
|
|
if let channel = view.peers[view.peerId] as? TelegramChannel {
|
|
if let adminRights = channel.adminRights, !adminRights.isEmpty {
|
|
canViewAdminsAndBanned = true
|
|
} else if channel.flags.contains(.isCreator) {
|
|
canViewAdminsAndBanned = true
|
|
}
|
|
}
|
|
|
|
if canViewAdminsAndBanned {
|
|
var activePermissionCount: Int?
|
|
if let defaultBannedRights = channel.defaultBannedRights {
|
|
var count = 0
|
|
for right in allGroupPermissionList {
|
|
if !defaultBannedRights.flags.contains(right) {
|
|
count += 1
|
|
}
|
|
}
|
|
activePermissionCount = count
|
|
}
|
|
|
|
entries.append(GroupInfoEntry.permissions(presentationData.theme, presentationData.strings.GroupInfo_Permissions, activePermissionCount.flatMap({ "\($0)/\(allGroupPermissionList.count)" }) ?? ""))
|
|
entries.append(GroupInfoEntry.administrators(presentationData.theme, presentationData.strings.GroupInfo_Administrators, cachedChannelData.participantsSummary.adminCount.flatMap { "\(presentationStringsFormattedNumber($0, presentationData.dateTimeFormat.groupingSeparator))" } ?? ""))
|
|
}
|
|
}
|
|
} else {
|
|
if let cachedChannelData = view.cachedData as? CachedChannelData {
|
|
if let about = cachedChannelData.about, !about.isEmpty {
|
|
entries.append(.aboutHeader(presentationData.theme, presentationData.strings.Channel_About_Title.uppercased()))
|
|
entries.append(.about(presentationData.theme, about))
|
|
}
|
|
if let peer = view.peers[view.peerId] as? TelegramChannel, let username = peer.username, !username.isEmpty {
|
|
entries.append(.link(presentationData.theme, "t.me/" + username))
|
|
}
|
|
} else if let cachedGroupData = view.cachedData as? CachedGroupData {
|
|
if let about = cachedGroupData.about, !about.isEmpty {
|
|
entries.append(.aboutHeader(presentationData.theme, presentationData.strings.Channel_About_Title.uppercased()))
|
|
entries.append(.about(presentationData.theme, about))
|
|
}
|
|
}
|
|
|
|
entries.append(GroupInfoEntry.notifications(presentationData.theme, presentationData.strings.GroupInfo_Notifications, notificationsText))
|
|
entries.append(GroupInfoEntry.sharedMedia(presentationData.theme, presentationData.strings.GroupInfo_SharedMedia))
|
|
}
|
|
|
|
var canRemoveAnyMember = false
|
|
if let cachedGroupData = view.cachedData as? CachedGroupData, let participants = cachedGroupData.participants {
|
|
for participant in participants.participants {
|
|
if canRemoveParticipant(account: account, isAdmin: canEditMembers, participantId: participant.peerId, invitedBy: participant.invitedBy) {
|
|
canRemoveAnyMember = true
|
|
break
|
|
}
|
|
}
|
|
} else if let channel = view.peers[view.peerId] as? TelegramChannel {
|
|
for participant in channelMembers {
|
|
if canRemoveParticipant(account: account, channel: channel, participant: participant.participant) {
|
|
canRemoveAnyMember = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if canAddMembers {
|
|
entries.append(GroupInfoEntry.addMember(presentationData.theme, presentationData.strings.GroupInfo_AddParticipant, editing: state.editingState != nil && canRemoveAnyMember))
|
|
}
|
|
|
|
if let group = view.peers[view.peerId] as? TelegramGroup, let cachedGroupData = view.cachedData as? CachedGroupData, let participants = cachedGroupData.participants {
|
|
var updatedParticipants = participants.participants
|
|
let existingParticipantIds = Set(updatedParticipants.map { $0.peerId })
|
|
|
|
var peerPresences: [PeerId: PeerPresence] = view.peerPresences
|
|
var peers: [PeerId: Peer] = view.peers
|
|
var disabledPeerIds = state.removingParticipantIds
|
|
|
|
if !state.temporaryParticipants.isEmpty {
|
|
for participant in state.temporaryParticipants {
|
|
if !existingParticipantIds.contains(participant.peer.id) {
|
|
updatedParticipants.append(.member(id: participant.peer.id, invitedBy: account.peerId, invitedAt: participant.timestamp))
|
|
if let presence = participant.presence, peerPresences[participant.peer.id] == nil {
|
|
peerPresences[participant.peer.id] = presence
|
|
}
|
|
if peers[participant.peer.id] == nil {
|
|
peers[participant.peer.id] = participant.peer
|
|
}
|
|
disabledPeerIds.insert(participant.peer.id)
|
|
}
|
|
}
|
|
}
|
|
|
|
let sortedParticipants = updatedParticipants.sorted(by: { lhs, rhs in
|
|
let lhsPresence = peerPresences[lhs.peerId] as? TelegramUserPresence
|
|
let rhsPresence = peerPresences[rhs.peerId] as? TelegramUserPresence
|
|
if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence {
|
|
if lhsPresence.status < rhsPresence.status {
|
|
return false
|
|
} else if lhsPresence.status > rhsPresence.status {
|
|
return true
|
|
}
|
|
} else if let _ = lhsPresence {
|
|
return true
|
|
} else if let _ = rhsPresence {
|
|
return false
|
|
}
|
|
|
|
switch lhs {
|
|
case .creator:
|
|
return false
|
|
case let .admin(lhsId, _, lhsInvitedAt):
|
|
switch rhs {
|
|
case .creator:
|
|
return true
|
|
case let .admin(rhsId, _, rhsInvitedAt):
|
|
if lhsInvitedAt == rhsInvitedAt {
|
|
return lhsId.id < rhsId.id
|
|
}
|
|
return lhsInvitedAt > rhsInvitedAt
|
|
case let .member(rhsId, _, rhsInvitedAt):
|
|
if lhsInvitedAt == rhsInvitedAt {
|
|
return lhsId.id < rhsId.id
|
|
}
|
|
return lhsInvitedAt > rhsInvitedAt
|
|
}
|
|
case let .member(lhsId, _, lhsInvitedAt):
|
|
switch rhs {
|
|
case .creator:
|
|
return true
|
|
case let .admin(rhsId, _, rhsInvitedAt):
|
|
if lhsInvitedAt == rhsInvitedAt {
|
|
return lhsId.id < rhsId.id
|
|
}
|
|
return lhsInvitedAt > rhsInvitedAt
|
|
case let .member(rhsId, _, rhsInvitedAt):
|
|
if lhsInvitedAt == rhsInvitedAt {
|
|
return lhsId.id < rhsId.id
|
|
}
|
|
return lhsInvitedAt > rhsInvitedAt
|
|
}
|
|
}
|
|
})
|
|
|
|
for i in 0 ..< sortedParticipants.count {
|
|
if let peer = peers[sortedParticipants[i].peerId] {
|
|
let memberStatus: GroupInfoMemberStatus
|
|
let participant: ChannelParticipant
|
|
switch sortedParticipants[i] {
|
|
case .creator:
|
|
participant = .creator(id: sortedParticipants[i].peerId)
|
|
memberStatus = .admin
|
|
case .admin:
|
|
participant = .member(id: sortedParticipants[i].peerId, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(flags: .groupSpecific), promotedBy: account.peerId, canBeEditedByAccountPeer: true), banInfo: nil)
|
|
memberStatus = .admin
|
|
case .member:
|
|
participant = .member(id: sortedParticipants[i].peerId, invitedAt: 0, adminInfo: nil, banInfo: nil)
|
|
memberStatus = .member
|
|
}
|
|
|
|
var canPromote: Bool
|
|
var canRestrict: Bool
|
|
if sortedParticipants[i].peerId == account.peerId {
|
|
canPromote = false
|
|
canRestrict = false
|
|
} else {
|
|
switch group.role {
|
|
case .creator:
|
|
canPromote = true
|
|
canRestrict = true
|
|
case .member:
|
|
canPromote = false
|
|
switch sortedParticipants[i] {
|
|
case .creator, .admin:
|
|
canPromote = false
|
|
canRestrict = false
|
|
case let .member(member):
|
|
if member.invitedBy == account.peerId {
|
|
canRestrict = true
|
|
} else {
|
|
canRestrict = false
|
|
}
|
|
}
|
|
case .admin:
|
|
switch sortedParticipants[i] {
|
|
case .creator, .admin:
|
|
canPromote = false
|
|
canRestrict = false
|
|
case .member:
|
|
canPromote = false
|
|
canRestrict = true
|
|
}
|
|
}
|
|
}
|
|
|
|
var peerActions: [ParticipantRevealAction] = []
|
|
if canPromote {
|
|
peerActions.append(ParticipantRevealAction(type: .neutral, title: presentationData.strings.GroupInfo_ActionPromote, action: .promote))
|
|
}
|
|
if canRestrict {
|
|
peerActions.append(ParticipantRevealAction(type: .warning, title: presentationData.strings.GroupInfo_ActionRestrict, action: .restrict))
|
|
peerActions.append(ParticipantRevealAction(type: .destructive, title: presentationData.strings.Common_Delete, action: .remove))
|
|
}
|
|
|
|
entries.append(GroupInfoEntry.member(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, index: i, peerId: peer.id, peer: peer, participant: RenderedChannelParticipant(participant: participant, peer: peer), presence: peerPresences[peer.id], memberStatus: memberStatus, editing: ItemListPeerItemEditing(editable: canRemoveParticipant(account: account, isAdmin: canEditMembers, participantId: peer.id, invitedBy: sortedParticipants[i].invitedBy), editing: state.editingState != nil && canRemoveAnyMember, revealed: state.peerIdWithRevealedOptions == peer.id), revealActions: peerActions, enabled: !disabledPeerIds.contains(peer.id)))
|
|
}
|
|
}
|
|
} else if let channel = view.peers[view.peerId] as? TelegramChannel, let cachedChannelData = view.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount {
|
|
var updatedParticipants = channelMembers
|
|
let existingParticipantIds = Set(updatedParticipants.map { $0.peer.id })
|
|
|
|
var peerPresences: [PeerId: PeerPresence] = view.peerPresences
|
|
var peers: [PeerId: Peer] = view.peers
|
|
|
|
if !state.temporaryParticipants.isEmpty {
|
|
for participant in state.temporaryParticipants {
|
|
if !existingParticipantIds.contains(participant.peer.id) {
|
|
updatedParticipants.append(RenderedChannelParticipant(participant: ChannelParticipant.member(id: participant.peer.id, invitedAt: participant.timestamp, adminInfo: nil, banInfo: nil), peer: participant.peer))
|
|
if let presence = participant.presence, peerPresences[participant.peer.id] == nil {
|
|
peerPresences[participant.peer.id] = presence
|
|
}
|
|
if peers[participant.peer.id] == nil {
|
|
peers[participant.peer.id] = participant.peer
|
|
}
|
|
//disabledPeerIds.insert(participant.peer.id)
|
|
}
|
|
}
|
|
}
|
|
|
|
let sortedParticipants: [RenderedChannelParticipant]
|
|
if memberCount < 200 {
|
|
sortedParticipants = updatedParticipants.sorted(by: { lhs, rhs in
|
|
let lhsPresence = lhs.presences[lhs.peer.id] as? TelegramUserPresence
|
|
let rhsPresence = rhs.presences[rhs.peer.id] as? TelegramUserPresence
|
|
if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence {
|
|
if lhsPresence.status < rhsPresence.status {
|
|
return false
|
|
} else if lhsPresence.status > rhsPresence.status {
|
|
return true
|
|
}
|
|
} else if let _ = lhsPresence {
|
|
return true
|
|
} else if let _ = rhsPresence {
|
|
return false
|
|
}
|
|
|
|
switch lhs.participant {
|
|
case .creator:
|
|
return false
|
|
case let .member(lhsId, lhsInvitedAt, _, _):
|
|
switch rhs.participant {
|
|
case .creator:
|
|
return true
|
|
case let .member(rhsId, rhsInvitedAt, _, _):
|
|
if lhsInvitedAt == rhsInvitedAt {
|
|
return lhsId.id < rhsId.id
|
|
}
|
|
return lhsInvitedAt > rhsInvitedAt
|
|
}
|
|
}
|
|
})
|
|
} else {
|
|
sortedParticipants = updatedParticipants
|
|
}
|
|
|
|
for i in 0 ..< sortedParticipants.count {
|
|
let participant = sortedParticipants[i]
|
|
let memberStatus: GroupInfoMemberStatus
|
|
switch participant.participant {
|
|
case .creator:
|
|
memberStatus = .admin
|
|
case let .member(_, _, adminInfo, _):
|
|
if adminInfo != nil {
|
|
memberStatus = .admin
|
|
} else {
|
|
memberStatus = .member
|
|
}
|
|
}
|
|
|
|
var canPromote: Bool
|
|
var canRestrict: Bool
|
|
if participant.peer.id == account.peerId {
|
|
canPromote = false
|
|
canRestrict = false
|
|
} else {
|
|
switch participant.participant {
|
|
case .creator:
|
|
canPromote = false
|
|
canRestrict = false
|
|
case let .member(_, _, adminRights, bannedRights):
|
|
if channel.hasPermission(.addAdmins) {
|
|
canPromote = true
|
|
} else {
|
|
canPromote = false
|
|
}
|
|
if channel.hasPermission(.banMembers) {
|
|
canRestrict = true
|
|
} else {
|
|
canRestrict = false
|
|
}
|
|
if canPromote {
|
|
if let bannedRights = bannedRights {
|
|
if bannedRights.restrictedBy != account.peerId && !channel.flags.contains(.isCreator) {
|
|
canPromote = false
|
|
}
|
|
}
|
|
}
|
|
if canRestrict {
|
|
if let adminRights = adminRights {
|
|
if adminRights.promotedBy != account.peerId && !channel.flags.contains(.isCreator) {
|
|
canRestrict = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var peerActions: [ParticipantRevealAction] = []
|
|
if canPromote {
|
|
peerActions.append(ParticipantRevealAction(type: .neutral, title: presentationData.strings.GroupInfo_ActionPromote, action: .promote))
|
|
}
|
|
if canRestrict {
|
|
peerActions.append(ParticipantRevealAction(type: .warning, title: presentationData.strings.GroupInfo_ActionRestrict, action: .restrict))
|
|
peerActions.append(ParticipantRevealAction(type: .destructive, title: presentationData.strings.Common_Delete, action: .remove))
|
|
}
|
|
|
|
entries.append(GroupInfoEntry.member(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, index: i, peerId: participant.peer.id, peer: participant.peer, participant: participant, presence: participant.presences[participant.peer.id], memberStatus: memberStatus, editing: ItemListPeerItemEditing(editable: !peerActions.isEmpty, editing: state.editingState != nil && canRemoveAnyMember, revealed: state.peerIdWithRevealedOptions == participant.peer.id), revealActions: peerActions, enabled: true))
|
|
}
|
|
}
|
|
|
|
if let group = view.peers[view.peerId] as? TelegramGroup {
|
|
if case .Member = group.membership {
|
|
entries.append(.leave(presentationData.theme, presentationData.strings.Group_LeaveGroup))
|
|
}
|
|
} else if let channel = view.peers[view.peerId] as? TelegramChannel {
|
|
if case .member = channel.participationStatus {
|
|
if channel.flags.contains(.isCreator) {
|
|
if let cachedChannelData = view.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount, memberCount <= 200 {
|
|
if state.editingState != nil {
|
|
entries.append(.leave(presentationData.theme, presentationData.strings.ChannelInfo_DeleteGroup))
|
|
} else {
|
|
entries.append(.leave(presentationData.theme, presentationData.strings.Group_LeaveGroup))
|
|
}
|
|
}
|
|
} else {
|
|
entries.append(.leave(presentationData.theme, presentationData.strings.Group_LeaveGroup))
|
|
}
|
|
}
|
|
}
|
|
|
|
return entries
|
|
}
|
|
|
|
private func valuesRequiringUpdate(state: GroupInfoState, view: PeerView) -> (title: String?, description: String?) {
|
|
if let peer = view.peers[view.peerId] as? TelegramGroup {
|
|
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? CachedGroupData {
|
|
if let about = cachedData.about {
|
|
if about != editingState.editingDescriptionText {
|
|
descriptionValue = editingState.editingDescriptionText
|
|
}
|
|
} else if !editingState.editingDescriptionText.isEmpty {
|
|
descriptionValue = editingState.editingDescriptionText
|
|
}
|
|
}
|
|
}
|
|
return (titleValue, descriptionValue)
|
|
} else 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 groupInfoController(context: AccountContext, peerId originalPeerId: PeerId, membersLoaded: @escaping () -> Void = {}) -> ViewController {
|
|
let statePromise = ValuePromise(GroupInfoState(updatingAvatar: nil, editingState: nil, updatingName: nil, peerIdWithRevealedOptions: nil, temporaryParticipants: [], successfullyAddedParticipantIds: Set(), removingParticipantIds: Set(), savingData: false, searchingMembers: false), ignoreRepeated: true)
|
|
let stateValue = Atomic(value: GroupInfoState(updatingAvatar: nil, editingState: nil, updatingName: nil, peerIdWithRevealedOptions: nil, temporaryParticipants: [], successfullyAddedParticipantIds: Set(), removingParticipantIds: Set(), savingData: false, searchingMembers: false))
|
|
let updateState: ((GroupInfoState) -> GroupInfoState) -> Void = { f in
|
|
statePromise.set(stateValue.modify { f($0) })
|
|
}
|
|
|
|
var pushControllerImpl: ((ViewController) -> Void)?
|
|
var presentControllerImpl: ((ViewController, Any?) -> Void)?
|
|
var endEditingImpl: (() -> Void)?
|
|
var removePeerChatImpl: ((Peer, Bool) -> Void)?
|
|
|
|
let actionsDisposable = DisposableSet()
|
|
|
|
let updatePeerNameDisposable = MetaDisposable()
|
|
actionsDisposable.add(updatePeerNameDisposable)
|
|
|
|
let updatePeerDescriptionDisposable = MetaDisposable()
|
|
actionsDisposable.add(updatePeerDescriptionDisposable)
|
|
|
|
let addMemberDisposable = MetaDisposable()
|
|
actionsDisposable.add(addMemberDisposable)
|
|
let selectAddMemberDisposable = MetaDisposable()
|
|
actionsDisposable.add(selectAddMemberDisposable)
|
|
|
|
let removeMemberDisposable = MetaDisposable()
|
|
actionsDisposable.add(removeMemberDisposable)
|
|
|
|
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 upgradeDisposable = MetaDisposable()
|
|
actionsDisposable.add(upgradeDisposable)
|
|
|
|
var avatarGalleryTransitionArguments: ((AvatarGalleryEntry) -> GalleryTransitionArguments?)?
|
|
let avatarAndNameInfoContext = ItemListAvatarAndNameInfoItemContext()
|
|
var updateHiddenAvatarImpl: (() -> Void)?
|
|
|
|
var displayCopyContextMenuImpl: ((String, GroupInfoEntryTag) -> Void)?
|
|
var aboutLinkActionImpl: ((TextLinkItemActionType, TextLinkItem) -> Void)?
|
|
|
|
var upgradedToSupergroupImpl: ((PeerId, @escaping () -> Void) -> Void)?
|
|
|
|
let actualPeerId = Promise<PeerId>()
|
|
actualPeerId.set(context.account.viewTracker.peerView(originalPeerId)
|
|
|> map { peerView -> PeerId in
|
|
if let peer = peerView.peers[peerView.peerId] as? TelegramGroup, let migrationReference = peer.migrationReference {
|
|
return migrationReference.peerId
|
|
} else {
|
|
return originalPeerId
|
|
}
|
|
}
|
|
|> distinctUntilChanged)
|
|
|
|
let peerView = Promise<PeerView>()
|
|
let peerViewSignal = actualPeerId.get()
|
|
|> distinctUntilChanged
|
|
|> mapToSignal { peerId -> Signal<PeerView, NoError> in
|
|
return context.account.viewTracker.peerView(peerId)
|
|
}
|
|
peerView.set(peerViewSignal)
|
|
|
|
let arguments = GroupInfoArguments(context: context, avatarAndNameInfoContext: avatarAndNameInfoContext, tapAvatarAction: {
|
|
let _ = (peerView.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { peerView in
|
|
guard let peer = peerView.peers[peerView.peerId] else {
|
|
return
|
|
}
|
|
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 _ = (peerView.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { peerView in
|
|
let _ = (context.account.postbox.transaction { transaction -> (Peer?, SearchBotsConfiguration) in
|
|
return (transaction.getPeer(peerView.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 = UIImageJPEGRepresentation(image, 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: peerView.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, 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: peerView.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()
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}, pushController: { controller in
|
|
pushControllerImpl?(controller)
|
|
}, presentController: { controller, presentationArguments in
|
|
presentControllerImpl?(controller, presentationArguments)
|
|
}, changeNotificationMuteSettings: {
|
|
let _ = (peerView.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { peerView in
|
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
let _ = (context.account.postbox.transaction { transaction -> (TelegramPeerNotificationSettings, GlobalNotificationSettings) in
|
|
let peerSettings: TelegramPeerNotificationSettings = (transaction.getPeerNotificationSettings(peerView.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: peerView.peerId, sound: sound).start()
|
|
})
|
|
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
|
}, updateSettings: { value in
|
|
changeMuteSettingsDisposable.set(updatePeerMuteSetting(account: context.account, peerId: peerView.peerId, muteInterval: value).start())
|
|
})
|
|
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
|
})
|
|
})
|
|
}, openPreHistory: {
|
|
let _ = (peerView.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { peerView in
|
|
presentControllerImpl?(groupPreHistorySetupController(context: context, peerId: peerView.peerId, upgradedToSupergroup: { peerId, f in
|
|
upgradedToSupergroupImpl?(peerId, f)
|
|
}), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
|
})
|
|
}, openSharedMedia: {
|
|
let _ = (peerView.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { peerView in
|
|
if let controller = peerSharedMediaController(context: context, peerId: peerView.peerId) {
|
|
pushControllerImpl?(controller)
|
|
}
|
|
})
|
|
}, openAdministrators: {
|
|
let _ = (peerView.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { peerView in
|
|
pushControllerImpl?(channelAdminsController(context: context, peerId: peerView.peerId))
|
|
})
|
|
}, openPermissions: {
|
|
let _ = (peerView.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { peerView in
|
|
pushControllerImpl?(channelPermissionsController(context: context, peerId: peerView.peerId))
|
|
})
|
|
}, updateEditingName: { editingName in
|
|
updateState { state in
|
|
if let editingState = state.editingState {
|
|
return state.withUpdatedEditingState(GroupInfoEditingState(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
|
|
}
|
|
}, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
|
|
updateState { state in
|
|
if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) {
|
|
return state.withUpdatedPeerIdWithRevealedOptions(peerId)
|
|
} else {
|
|
return state
|
|
}
|
|
}
|
|
}, addMember: {
|
|
let _ = (peerView.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { peerView in
|
|
let members: Promise<[PeerId]> = Promise()
|
|
if peerView.peerId.namespace == Namespaces.Peer.CloudChannel {
|
|
var membersDisposable: Disposable?
|
|
let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.recent(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerView.peerId, updated: { listState in
|
|
members.set(.single(listState.list.map {$0.peer.id}))
|
|
membersDisposable?.dispose()
|
|
})
|
|
membersDisposable = disposable
|
|
} else {
|
|
members.set(.single([]))
|
|
}
|
|
|
|
let _ = (combineLatest(queue: .mainQueue(), context.account.postbox.loadedPeerWithId(peerView.peerId)
|
|
|> deliverOnMainQueue, members.get() |> take(1) |> deliverOnMainQueue)).start(next: { groupPeer, recentIds in
|
|
var confirmationImpl: ((PeerId) -> Signal<Bool, NoError>)?
|
|
var options: [ContactListAdditionalOption] = []
|
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
var inviteByLinkImpl: (() -> Void)?
|
|
|
|
var canCreateInviteLink = false
|
|
if let group = groupPeer as? TelegramGroup {
|
|
switch group.role {
|
|
case .creator, .admin:
|
|
canCreateInviteLink = true
|
|
default:
|
|
break
|
|
}
|
|
} else if let channel = groupPeer as? TelegramChannel {
|
|
if channel.hasPermission(.inviteMembers) {
|
|
canCreateInviteLink = true
|
|
}
|
|
}
|
|
|
|
if canCreateInviteLink {
|
|
options.append(ContactListAdditionalOption(title: presentationData.strings.GroupInfo_InviteByLink, icon: .generic(UIImage(bundleImageName: "Contact List/LinkActionIcon")!), action: {
|
|
inviteByLinkImpl?()
|
|
}))
|
|
}
|
|
|
|
let contactsController: ViewController
|
|
if peerView.peerId.namespace == Namespaces.Peer.CloudGroup {
|
|
contactsController = ContactSelectionController(context: context, autoDismiss: false, title: { $0.GroupInfo_AddParticipantTitle }, options: options, confirmation: { peer in
|
|
if let confirmationImpl = confirmationImpl, case let .peer(peer, _) = peer {
|
|
return confirmationImpl(peer.id)
|
|
} else {
|
|
return .single(false)
|
|
}
|
|
})
|
|
} else {
|
|
contactsController = ContactMultiselectionController(context: context, mode: .peerSelection(searchChatList: false), options: options, filters: [.excludeSelf, .disable(recentIds)])
|
|
}
|
|
|
|
confirmationImpl = { [weak contactsController] peerId in
|
|
return context.account.postbox.loadedPeerWithId(peerId)
|
|
|> deliverOnMainQueue
|
|
|> mapToSignal { peer in
|
|
let result = ValuePromise<Bool>()
|
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
if let contactsController = contactsController {
|
|
let alertController = textAlertController(context: context, title: nil, text: presentationData.strings.GroupInfo_AddParticipantConfirmation(peer.displayTitle).0, actions: [
|
|
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_No, action: {
|
|
result.set(false)
|
|
}),
|
|
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Yes, action: {
|
|
result.set(true)
|
|
})
|
|
])
|
|
contactsController.present(alertController, in: .window(.root))
|
|
}
|
|
|
|
return result.get()
|
|
}
|
|
}
|
|
|
|
let addMember: (ContactListPeer) -> Signal<Void, NoError> = { memberPeer -> Signal<Void, NoError> in
|
|
if case let .peer(selectedPeer, _) = memberPeer {
|
|
let memberId = selectedPeer.id
|
|
if peerView.peerId.namespace == Namespaces.Peer.CloudChannel {
|
|
return context.peerChannelMemberCategoriesContextsManager.addMember(account: context.account, peerId: peerView.peerId, memberId: memberId)
|
|
}
|
|
|
|
if let peer = peerView.peers[memberId] {
|
|
updateState { state in
|
|
var found = false
|
|
for participant in state.temporaryParticipants {
|
|
if participant.peer.id == memberId {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
|
|
var temporaryParticipants = state.temporaryParticipants
|
|
temporaryParticipants.append(TemporaryParticipant(peer: peer, presence: peerView.peerPresences[memberId], timestamp: timestamp))
|
|
return state.withUpdatedTemporaryParticipants(temporaryParticipants)
|
|
} else {
|
|
return state
|
|
}
|
|
}
|
|
}
|
|
|
|
return addGroupMember(account: context.account, peerId: peerView.peerId, memberId: memberId)
|
|
|> deliverOnMainQueue
|
|
|> afterCompleted {
|
|
updateState { state in
|
|
var successfullyAddedParticipantIds = state.successfullyAddedParticipantIds
|
|
successfullyAddedParticipantIds.insert(memberId)
|
|
|
|
return state.withUpdatedSuccessfullyAddedParticipantIds(successfullyAddedParticipantIds)
|
|
}
|
|
}
|
|
|> `catch` { error -> Signal<Void, NoError> in
|
|
switch error {
|
|
case .generic:
|
|
updateState { state in
|
|
var temporaryParticipants = state.temporaryParticipants
|
|
for i in 0 ..< temporaryParticipants.count {
|
|
if temporaryParticipants[i].peer.id == memberId {
|
|
temporaryParticipants.remove(at: i)
|
|
break
|
|
}
|
|
}
|
|
var successfullyAddedParticipantIds = state.successfullyAddedParticipantIds
|
|
successfullyAddedParticipantIds.remove(memberId)
|
|
|
|
return state.withUpdatedTemporaryParticipants(temporaryParticipants).withUpdatedSuccessfullyAddedParticipantIds(successfullyAddedParticipantIds)
|
|
}
|
|
return .complete()
|
|
case .groupFull:
|
|
let signal = convertGroupToSupergroup(account: context.account, peerId: peerView.peerId)
|
|
|> map(Optional.init)
|
|
|> `catch` { _ -> Signal<PeerId?, NoError> in
|
|
return .single(nil)
|
|
}
|
|
|> mapToSignal { upgradedPeerId -> Signal<PeerId?, NoError> in
|
|
guard let upgradedPeerId = upgradedPeerId else {
|
|
return .single(nil)
|
|
}
|
|
return context.peerChannelMemberCategoriesContextsManager.addMember(account: context.account, peerId: upgradedPeerId, memberId: memberId)
|
|
|> mapToSignal { _ -> Signal<PeerId?, NoError> in
|
|
return .complete()
|
|
}
|
|
|> then(.single(upgradedPeerId))
|
|
}
|
|
|> deliverOnMainQueue
|
|
|> mapToSignal { upgradedPeerId -> Signal<Void, NoError> in
|
|
if let upgradedPeerId = upgradedPeerId {
|
|
upgradedToSupergroupImpl?(upgradedPeerId, {})
|
|
}
|
|
return .complete()
|
|
}
|
|
return signal
|
|
}
|
|
}
|
|
} else {
|
|
return .complete()
|
|
}
|
|
}
|
|
|
|
let addMembers: ([ContactListPeerId]) -> Signal<Void, AddChannelMemberError> = { members -> Signal<Void, AddChannelMemberError> in
|
|
let memberIds = members.compactMap { contact -> PeerId? in
|
|
switch contact {
|
|
case let .peer(peerId):
|
|
return peerId
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
return context.account.postbox.multiplePeersView(memberIds)
|
|
|> take(1)
|
|
|> deliverOnMainQueue
|
|
|> mapError { _ in return .generic}
|
|
|> mapToSignal { view -> Signal<Void, AddChannelMemberError> in
|
|
return context.peerChannelMemberCategoriesContextsManager.addMembers(account: context.account, peerId: peerView.peerId, memberIds: memberIds) |> map { _ in
|
|
updateState { state in
|
|
var state = state
|
|
for (memberId, peer) in view.peers {
|
|
var found = false
|
|
for participant in state.temporaryParticipants {
|
|
if participant.peer.id == memberId {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
|
|
var temporaryParticipants = state.temporaryParticipants
|
|
temporaryParticipants.append(TemporaryParticipant(peer: peer, presence: view.presences[memberId], timestamp: timestamp))
|
|
state = state.withUpdatedTemporaryParticipants(temporaryParticipants)
|
|
}
|
|
}
|
|
|
|
return state
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
inviteByLinkImpl = { [weak contactsController] in
|
|
contactsController?.dismiss()
|
|
|
|
presentControllerImpl?(channelVisibilityController(context: context, peerId: peerView.peerId, mode: .privateLink, upgradedToSupergroup: { updatedPeerId, f in
|
|
upgradedToSupergroupImpl?(updatedPeerId, f)
|
|
}), ViewControllerPresentationArguments(presentationAnimation: ViewControllerPresentationAnimation.modalSheet))
|
|
}
|
|
|
|
presentControllerImpl?(contactsController, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
|
if let contactsController = contactsController as? ContactSelectionController {
|
|
selectAddMemberDisposable.set((contactsController.result
|
|
|> deliverOnMainQueue).start(next: { [weak contactsController] memberPeer in
|
|
guard let memberPeer = memberPeer else {
|
|
return
|
|
}
|
|
|
|
contactsController?.displayProgress = true
|
|
addMemberDisposable.set((addMember(memberPeer)
|
|
|> deliverOnMainQueue).start(completed: {
|
|
contactsController?.dismiss()
|
|
}))
|
|
}))
|
|
contactsController.dismissed = {
|
|
selectAddMemberDisposable.set(nil)
|
|
addMemberDisposable.set(nil)
|
|
}
|
|
}
|
|
if let contactsController = contactsController as? ContactMultiselectionController {
|
|
selectAddMemberDisposable.set((contactsController.result
|
|
|> deliverOnMainQueue).start(next: { [weak contactsController] peers in
|
|
contactsController?.displayProgress = true
|
|
addMemberDisposable.set((addMembers(peers)
|
|
|> deliverOnMainQueue).start(error: { error in
|
|
if peers.count == 1, error == .restricted {
|
|
switch peers[0] {
|
|
case let .peer(peerId):
|
|
_ = (context.account.postbox.loadedPeerWithId(peerId) |> deliverOnMainQueue).start(next: { peer in
|
|
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Privacy_GroupsAndChannels_InviteToGroupError(peer.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
|
})
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
contactsController?.dismiss()
|
|
},completed: {
|
|
contactsController?.dismiss()
|
|
}))
|
|
}))
|
|
contactsController.dismissed = {
|
|
selectAddMemberDisposable.set(nil)
|
|
addMemberDisposable.set(nil)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}, promotePeer: { participant in
|
|
let _ = (peerView.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { peerView in
|
|
presentControllerImpl?(channelAdminController(context: context, peerId: peerView.peerId, adminId: participant.peer.id, initialParticipant: participant.participant, updated: { _ in
|
|
}, upgradedToSupergroup: { upgradedPeerId, f in
|
|
upgradedToSupergroupImpl?(upgradedPeerId, f)
|
|
}), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
|
})
|
|
}, restrictPeer: { participant in
|
|
let _ = (peerView.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { peerView in
|
|
presentControllerImpl?(channelBannedMemberController(context: context, peerId: peerView.peerId, memberId: participant.peer.id, initialParticipant: participant.participant, updated: { _ in
|
|
}, upgradedToSupergroup: { upgradedPeerId, f in
|
|
upgradedToSupergroupImpl?(upgradedPeerId, f)
|
|
}), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
|
})
|
|
}, removePeer: { memberId in
|
|
let _ = (peerView.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { peerView in
|
|
let signal = context.account.postbox.loadedPeerWithId(memberId)
|
|
|> deliverOnMainQueue
|
|
|> mapToSignal { peer -> Signal<Bool, NoError> in
|
|
let result = ValuePromise<Bool>()
|
|
result.set(true)
|
|
return result.get()
|
|
}
|
|
|> mapToSignal { value -> Signal<Void, NoError> in
|
|
if value {
|
|
updateState { state in
|
|
var temporaryParticipants = state.temporaryParticipants
|
|
for i in 0 ..< state.temporaryParticipants.count {
|
|
if state.temporaryParticipants[i].peer.id == memberId {
|
|
temporaryParticipants.remove(at: i)
|
|
break
|
|
}
|
|
}
|
|
var successfullyAddedParticipantIds = state.successfullyAddedParticipantIds
|
|
successfullyAddedParticipantIds.remove(memberId)
|
|
|
|
var removingParticipantIds = state.removingParticipantIds
|
|
removingParticipantIds.insert(memberId)
|
|
|
|
return state.withUpdatedTemporaryParticipants(temporaryParticipants).withUpdatedSuccessfullyAddedParticipantIds(successfullyAddedParticipantIds).withUpdatedRemovingParticipantIds(removingParticipantIds)
|
|
}
|
|
|
|
if peerView.peerId.namespace == Namespaces.Peer.CloudChannel {
|
|
return context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(account: context.account, peerId: peerView.peerId, memberId: memberId, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max))
|
|
|> afterDisposed {
|
|
Queue.mainQueue().async {
|
|
updateState { state in
|
|
var removingParticipantIds = state.removingParticipantIds
|
|
removingParticipantIds.remove(memberId)
|
|
|
|
return state.withUpdatedRemovingParticipantIds(removingParticipantIds)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return removePeerMember(account: context.account, peerId: peerView.peerId, memberId: memberId)
|
|
|> deliverOnMainQueue
|
|
|> afterDisposed {
|
|
updateState { state in
|
|
var removingParticipantIds = state.removingParticipantIds
|
|
removingParticipantIds.remove(memberId)
|
|
|
|
return state.withUpdatedRemovingParticipantIds(removingParticipantIds)
|
|
}
|
|
}
|
|
} else {
|
|
return .complete()
|
|
}
|
|
}
|
|
removeMemberDisposable.set(signal.start())
|
|
})
|
|
}, leave: {
|
|
let _ = (peerView.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { peerView in
|
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
|
|
if let channel = peerView.peers[peerView.peerId] as? TelegramChannel, channel.flags.contains(.isCreator), stateValue.with({ $0 }).editingState != nil {
|
|
let controller = ActionSheetController(presentationTheme: presentationData.theme)
|
|
let dismissAction: () -> Void = { [weak controller] in
|
|
controller?.dismissAnimated()
|
|
}
|
|
|
|
var items: [ActionSheetItem] = []
|
|
items.append(ActionSheetTextItem(title: presentationData.strings.ChannelInfo_DeleteGroupConfirmation))
|
|
items.append(ActionSheetButtonItem(title: presentationData.strings.ChannelInfo_DeleteGroup, color: .destructive, action: {
|
|
dismissAction()
|
|
removePeerChatImpl?(channel, true)
|
|
}))
|
|
controller.setItemGroups([
|
|
ActionSheetItemGroup(items: items),
|
|
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
|
|
])
|
|
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
|
} else if let peer = peerView.peers[peerView.peerId] {
|
|
let controller = ActionSheetController(presentationTheme: presentationData.theme)
|
|
let dismissAction: () -> Void = { [weak controller] in
|
|
controller?.dismissAnimated()
|
|
}
|
|
|
|
var items: [ActionSheetItem] = []
|
|
if peerView.peerId.namespace == Namespaces.Peer.CloudGroup {
|
|
items.append(ActionSheetTextItem(title: presentationData.strings.GroupInfo_DeleteAndExitConfirmation))
|
|
}
|
|
items.append(ActionSheetButtonItem(title: presentationData.strings.Group_LeaveGroup, color: .destructive, action: {
|
|
dismissAction()
|
|
removePeerChatImpl?(peer, false)
|
|
}))
|
|
controller.setItemGroups([
|
|
ActionSheetItemGroup(items: items),
|
|
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
|
|
])
|
|
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
|
}
|
|
})
|
|
}, displayUsernameShareMenu: { text in
|
|
let shareController = ShareController(context: context, subject: .url(text))
|
|
presentControllerImpl?(shareController, nil)
|
|
}, displayUsernameContextMenu: { text in
|
|
displayCopyContextMenuImpl?(text, .link)
|
|
}, displayAboutContextMenu: { text in
|
|
displayCopyContextMenuImpl?(text, .about)
|
|
}, aboutLinkAction: { action, itemLink in
|
|
aboutLinkActionImpl?(action, itemLink)
|
|
}, openStickerPackSetup: {
|
|
let _ = (peerView.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { peerView in
|
|
let _ = (context.account.postbox.transaction { transaction -> StickerPackCollectionInfo? in
|
|
return (transaction.getPeerCachedData(peerId: peerView.peerId) as? CachedChannelData)?.stickerPack
|
|
}
|
|
|> deliverOnMainQueue).start(next: { stickerPack in
|
|
presentControllerImpl?(groupStickerPackSetupController(context: context, peerId: peerView.peerId, currentPackInfo: stickerPack), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
|
})
|
|
})
|
|
}, openGroupTypeSetup: {
|
|
let _ = (peerView.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { peerView in
|
|
presentControllerImpl?(channelVisibilityController(context: context, peerId: peerView.peerId, mode: .generic, upgradedToSupergroup: { updatedPeerId, f in
|
|
upgradedToSupergroupImpl?(updatedPeerId, f)
|
|
}), ViewControllerPresentationArguments(presentationAnimation: ViewControllerPresentationAnimation.modalSheet))
|
|
})
|
|
})
|
|
|
|
let loadMoreControl = Atomic<(PeerId, PeerChannelMemberCategoryControl)?>(value: nil)
|
|
let channelMembersPromise = Promise<[RenderedChannelParticipant]>()
|
|
|
|
let channelMembersDisposable = MetaDisposable()
|
|
actionsDisposable.add(channelMembersDisposable)
|
|
|
|
var membersLoadedCalled = false
|
|
|
|
actionsDisposable.add((actualPeerId.get()
|
|
|> distinctUntilChanged
|
|
|> deliverOnMainQueue).start(next: { peerId in
|
|
if peerId.namespace == Namespaces.Peer.CloudChannel {
|
|
let (disposable, control) = context.peerChannelMemberCategoriesContextsManager.recent(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, updated: { state in
|
|
channelMembersPromise.set(.single(state.list))
|
|
if case .loading(true) = state.loadingState {
|
|
} else if !membersLoadedCalled {
|
|
membersLoadedCalled = true
|
|
membersLoaded()
|
|
}
|
|
})
|
|
if let control = control {
|
|
let _ = loadMoreControl.swap((peerId, control))
|
|
} else {
|
|
let _ = loadMoreControl.swap(nil)
|
|
}
|
|
channelMembersDisposable.set(disposable)
|
|
} else {
|
|
let _ = loadMoreControl.swap(nil)
|
|
channelMembersPromise.set(.single([]))
|
|
channelMembersDisposable.set(nil)
|
|
if !membersLoadedCalled {
|
|
membersLoadedCalled = true
|
|
membersLoaded()
|
|
}
|
|
}
|
|
}))
|
|
|
|
let previousStateValue = Atomic<GroupInfoState?>(value: nil)
|
|
let previousChannelMembers = Atomic<[PeerId]?>(value: nil)
|
|
|
|
let searchContext = GroupMembersSearchContext(context: context, peerId: originalPeerId)
|
|
|
|
let globalNotificationsKey: PostboxViewKey = .preferences(keys: Set<ValueBoxKey>([PreferencesKeys.globalNotifications]))
|
|
let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get(), peerView.get(), context.account.postbox.combinedView(keys: [globalNotificationsKey]), channelMembersPromise.get())
|
|
|> map { presentationData, state, view, combinedView, channelMembers -> (ItemListControllerState, (ItemListNodeState<GroupInfoEntry>, GroupInfoEntry.ItemGenerationArguments)) 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 canEditGroupInfo = false
|
|
if let group = view.peers[view.peerId] as? TelegramGroup {
|
|
switch group.role {
|
|
case .admin, .creator:
|
|
canEditGroupInfo = true
|
|
case .member:
|
|
break
|
|
}
|
|
if !group.hasBannedPermission(.banChangeInfo) {
|
|
canEditGroupInfo = true
|
|
}
|
|
} else if let channel = view.peers[view.peerId] as? TelegramChannel {
|
|
if channel.hasPermission(.changeInfo) || !(channel.adminRights?.flags ?? []).isEmpty {
|
|
canEditGroupInfo = true
|
|
}
|
|
}
|
|
|
|
var rightNavigationButton: ItemListNavigationButton?
|
|
var secondaryRightNavigationButton: ItemListNavigationButton?
|
|
if let editingState = state.editingState {
|
|
var doneEnabled = true
|
|
if let editingName = editingState.editingName, editingName.isEmpty {
|
|
doneEnabled = false
|
|
}
|
|
if editingState.editingDescriptionText.count > 255 {
|
|
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)
|
|
updateState { state in
|
|
updateValues = valuesRequiringUpdate(state: state, view: view)
|
|
if updateValues.0 != nil || updateValues.1 != nil {
|
|
return state.withUpdatedSavingData(true)
|
|
} else {
|
|
return state.withUpdatedEditingState(nil)
|
|
}
|
|
}
|
|
|
|
let updateTitle: Signal<Void, Void>
|
|
if let titleValue = updateValues.title {
|
|
updateTitle = updatePeerTitle(account: context.account, peerId: view.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: view.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 canEditGroupInfo {
|
|
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: {
|
|
if let peer = peer as? TelegramGroup {
|
|
var text = ""
|
|
if let cachedData = view.cachedData as? CachedGroupData, let about = cachedData.about {
|
|
text = about
|
|
}
|
|
updateState { state in
|
|
return state.withUpdatedEditingState(GroupInfoEditingState(editingName: ItemListAvatarAndNameInfoItemName(peer), editingDescriptionText: text))
|
|
}
|
|
} else if let channel = peer as? TelegramChannel, case .group = channel.info {
|
|
var text = ""
|
|
if let cachedData = view.cachedData as? CachedChannelData, let about = cachedData.about {
|
|
text = about
|
|
}
|
|
updateState { state in
|
|
return state.withUpdatedEditingState(GroupInfoEditingState(editingName: ItemListAvatarAndNameInfoItemName(channel), editingDescriptionText: text))
|
|
}
|
|
}
|
|
})
|
|
if peer is TelegramChannel {
|
|
secondaryRightNavigationButton = ItemListNavigationButton(content: .icon(.search), style: .regular, enabled: true, action: {
|
|
updateState { state in
|
|
return state.withUpdatedSearchingMembers(true)
|
|
}
|
|
})
|
|
}
|
|
} else {
|
|
if peer is TelegramChannel {
|
|
rightNavigationButton = ItemListNavigationButton(content: .icon(.search), style: .regular, enabled: true, action: {
|
|
updateState { state in
|
|
return state.withUpdatedSearchingMembers(true)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
var searchItem: ItemListControllerSearch?
|
|
if state.searchingMembers {
|
|
searchItem = ChannelMembersSearchItem(context: context, peerId: view.peerId, searchContext: searchContext, cancel: {
|
|
updateState { state in
|
|
return state.withUpdatedSearchingMembers(false)
|
|
}
|
|
}, openPeer: { peer, _ in
|
|
if let infoController = peerInfoController(context: context, peer: peer) {
|
|
arguments.pushController(infoController)
|
|
}
|
|
}, present: { c, a in
|
|
presentControllerImpl?(c, a)
|
|
})
|
|
}
|
|
|
|
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.GroupInfo_Title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, secondaryRightNavigationButton: secondaryRightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back))
|
|
|
|
let entries = groupInfoEntries(account: context.account, presentationData: presentationData, view: view, channelMembers: channelMembers, globalNotificationSettings: globalNotificationSettings, state: state)
|
|
var memberIds: [PeerId] = []
|
|
for entry in entries {
|
|
switch entry {
|
|
case let .member(member):
|
|
memberIds.append(member.peerId)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
let previousState = previousStateValue.swap(state)
|
|
let previousMembers = previousChannelMembers.swap(memberIds) ?? []
|
|
|
|
var animateChanges = previousMembers.count > memberIds.count || (previousState != nil && (previousState!.editingState != nil) != (state.editingState != nil))
|
|
if presentationData.disableAnimations {
|
|
if Set(memberIds) == Set(previousMembers) && memberIds != previousMembers {
|
|
animateChanges = false
|
|
}
|
|
}
|
|
|
|
let listState = ItemListNodeState(entries: entries, style: .blocks, searchItem: searchItem, animateChanges: animateChanges)
|
|
|
|
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?.view.endEditing(true)
|
|
controller?.present(value, in: .window(.root), with: presentationArguments)
|
|
}
|
|
upgradedToSupergroupImpl = { [weak controller] upgradedPeerId, f in
|
|
let _ = (context.account.postbox.transaction { transaction -> Peer? in
|
|
return transaction.getPeer(upgradedPeerId)
|
|
}
|
|
|> deliverOnMainQueue).start(next: { peer in
|
|
guard let controller = controller, let navigationController = controller.navigationController as? NavigationController, let _ = peer else {
|
|
return
|
|
}
|
|
let infoController = groupInfoController(context: context, peerId: upgradedPeerId, membersLoaded: {
|
|
f()
|
|
})
|
|
let chatController = ChatController(context: context, chatLocation: .peer(upgradedPeerId), messageId: nil, botStart: nil, mode: .standard(previewing: false))
|
|
var viewControllers: [UIViewController] = []
|
|
if let first = navigationController.viewControllers.first {
|
|
viewControllers.append(first)
|
|
}
|
|
viewControllers.append(chatController)
|
|
viewControllers.append(infoController)
|
|
navigationController.setViewControllers(viewControllers, animated: false)
|
|
})
|
|
}
|
|
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] removed in
|
|
if removed {
|
|
navigationController?.popToRoot(animated: true)
|
|
}
|
|
}, removed: {
|
|
})
|
|
break
|
|
}
|
|
}
|
|
}
|
|
displayCopyContextMenuImpl = { [weak controller] text, tag in
|
|
if let strongController = controller {
|
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
var resultItemNode: ListViewItemNode?
|
|
let _ = strongController.frameForItemNode({ itemNode in
|
|
var itemTag: GroupInfoEntryTag? = nil
|
|
if let itemNode = itemNode as? ItemListMultilineTextItemNode {
|
|
if let tag = itemNode.tag as? GroupInfoEntryTag {
|
|
itemTag = tag
|
|
}
|
|
}
|
|
else if let itemNode = itemNode as? ItemListActionItemNode {
|
|
if let tag = itemNode.tag as? GroupInfoEntryTag {
|
|
itemTag = tag
|
|
}
|
|
}
|
|
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 controller] action, itemLink in
|
|
let _ = (peerView.get()
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { peerView in
|
|
if let controller = controller {
|
|
handlePeerInfoAboutTextAction(context: context, peerId: peerView.peerId, navigateDisposable: navigateDisposable, controller: controller, action: action, itemLink: itemLink)
|
|
}
|
|
})
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
endEditingImpl = {
|
|
[weak controller] in
|
|
controller?.view.endEditing(true)
|
|
}
|
|
controller.visibleBottomContentOffsetChanged = { offset in
|
|
if let (peerId, loadMoreControl) = loadMoreControl.with({ $0 }), case let .known(value) = offset, value < 40.0 {
|
|
context.peerChannelMemberCategoriesContextsManager.loadMore(peerId: peerId, control: loadMoreControl)
|
|
}
|
|
}
|
|
return controller
|
|
}
|
|
|
|
func handlePeerInfoAboutTextAction(context: AccountContext, peerId: PeerId, navigateDisposable: MetaDisposable, controller: ViewController, action: TextLinkItemActionType, itemLink: TextLinkItem) {
|
|
let presentImpl: (ViewController, Any?) -> Void = { controllerToPresent, _ in
|
|
controller.present(controllerToPresent, in: .window(.root))
|
|
}
|
|
|
|
let openResolvedPeerImpl: (PeerId?, ChatControllerInteractionNavigateToPeer) -> Void = { [weak controller] peerId, navigation in
|
|
openResolvedUrl(.peer(peerId, navigation), context: context, navigationController: (controller?.navigationController as? NavigationController), openPeer: { (peerId, navigation) in
|
|
switch navigation {
|
|
case let .chat(_, messageId):
|
|
if let navigationController = controller?.navigationController as? NavigationController {
|
|
navigateToChatController(navigationController: navigationController, context: context, chatLocation: .peer(peerId), messageId: messageId, keepStack: .always)
|
|
}
|
|
case .info:
|
|
let peerSignal: Signal<Peer?, NoError>
|
|
peerSignal = context.account.postbox.loadedPeerWithId(peerId) |> map(Optional.init)
|
|
navigateDisposable.set((peerSignal |> take(1) |> deliverOnMainQueue).start(next: { peer in
|
|
if let controller = controller, let peer = peer {
|
|
if let infoController = peerInfoController(context: context, peer: peer) {
|
|
(controller.navigationController as? NavigationController)?.pushViewController(infoController)
|
|
}
|
|
}
|
|
}))
|
|
default:
|
|
break
|
|
}
|
|
}, present: presentImpl, dismissInput: {})
|
|
}
|
|
|
|
let openLinkImpl: (String) -> Void = { [weak controller] url in
|
|
navigateDisposable.set((resolveUrl(account: context.account, url: url) |> deliverOnMainQueue).start(next: { result in
|
|
if let controller = controller {
|
|
switch result {
|
|
case let .externalUrl(url):
|
|
context.sharedContext.applicationBindings.openUrl(url)
|
|
case let .peer(peerId, _):
|
|
openResolvedPeerImpl(peerId, .default)
|
|
case let .channelMessage(peerId, messageId):
|
|
if let navigationController = controller.navigationController as? NavigationController {
|
|
navigateToChatController(navigationController: navigationController, context: context, chatLocation: .peer(peerId), messageId: messageId)
|
|
}
|
|
case let .stickerPack(name):
|
|
controller.present(StickerPackPreviewController(context: context, stickerPack: .name(name), parentNavigationController: controller.navigationController as? NavigationController), in: .window(.root))
|
|
case let .instantView(webpage, anchor):
|
|
(controller.navigationController as? NavigationController)?.pushViewController(InstantPageController(context: context, webPage: webpage, sourcePeerType: .group, anchor: anchor))
|
|
case let .join(link):
|
|
controller.present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peerId in
|
|
openResolvedPeerImpl(peerId, .chat(textInputState: nil, messageId: nil))
|
|
}), in: .window(.root))
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}))
|
|
}
|
|
|
|
let openPeerMentionImpl: (String) -> Void = { mention in
|
|
navigateDisposable.set((resolvePeerByName(account: context.account, name: mention, ageLimit: 10) |> take(1) |> deliverOnMainQueue).start(next: { peerId in
|
|
openResolvedPeerImpl(peerId, .default)
|
|
}))
|
|
}
|
|
|
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
switch action {
|
|
case .tap:
|
|
switch itemLink {
|
|
case let .url(url):
|
|
openLinkImpl(url)
|
|
case let .mention(mention):
|
|
openPeerMentionImpl(mention)
|
|
case let .hashtag(_, hashtag):
|
|
let peerSignal = context.account.postbox.loadedPeerWithId(peerId)
|
|
let _ = (peerSignal
|
|
|> deliverOnMainQueue).start(next: { peer in
|
|
let searchController = HashtagSearchController(context: context, peer: peer, query: hashtag)
|
|
(controller.navigationController as? NavigationController)?.pushViewController(searchController)
|
|
})
|
|
}
|
|
case .longTap:
|
|
switch itemLink {
|
|
case let .url(url):
|
|
let canOpenIn = availableOpenInOptions(context: context, item: .url(url: url)).count > 1
|
|
let openText = canOpenIn ? presentationData.strings.Conversation_FileOpenIn : presentationData.strings.Conversation_LinkDialogOpen
|
|
let actionSheet = ActionSheetController(presentationTheme: presentationData.theme)
|
|
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
|
ActionSheetTextItem(title: url),
|
|
ActionSheetButtonItem(title: openText, color: .accent, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
openLinkImpl(url)
|
|
}),
|
|
ActionSheetButtonItem(title: presentationData.strings.ShareMenu_CopyShareLink, color: .accent, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
UIPasteboard.general.string = url
|
|
}),
|
|
ActionSheetButtonItem(title: presentationData.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
if let link = URL(string: url) {
|
|
let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil)
|
|
}
|
|
})
|
|
]), ActionSheetItemGroup(items: [
|
|
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
})
|
|
])])
|
|
controller.present(actionSheet, in: .window(.root))
|
|
case let .mention(mention):
|
|
let actionSheet = ActionSheetController(presentationTheme: presentationData.theme)
|
|
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
|
ActionSheetTextItem(title: mention),
|
|
ActionSheetButtonItem(title: presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
openPeerMentionImpl(mention)
|
|
}),
|
|
ActionSheetButtonItem(title: presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
UIPasteboard.general.string = mention
|
|
})
|
|
]), ActionSheetItemGroup(items: [
|
|
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
})
|
|
])])
|
|
controller.present(actionSheet, in: .window(.root))
|
|
case let .hashtag(_, hashtag):
|
|
let actionSheet = ActionSheetController(presentationTheme: presentationData.theme)
|
|
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
|
ActionSheetTextItem(title: hashtag),
|
|
ActionSheetButtonItem(title: presentationData.strings.Conversation_LinkDialogOpen, color: .accent, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
let searchController = HashtagSearchController(context: context, peer: nil, query: hashtag)
|
|
(controller.navigationController as? NavigationController)?.pushViewController(searchController)
|
|
}),
|
|
ActionSheetButtonItem(title: presentationData.strings.Conversation_LinkDialogCopy, color: .accent, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
UIPasteboard.general.string = hashtag
|
|
})
|
|
]), ActionSheetItemGroup(items: [
|
|
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
})
|
|
])])
|
|
controller.present(actionSheet, in: .window(.root))
|
|
}
|
|
}
|
|
}
|