mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
556 lines
22 KiB
Swift
556 lines
22 KiB
Swift
import Foundation
|
|
import Display
|
|
import SwiftSignalKit
|
|
import Postbox
|
|
import TelegramCore
|
|
|
|
private struct ChannelVisibilityControllerArguments {
|
|
let account: Account
|
|
|
|
let updateCurrentType: (CurrentChannelType) -> Void
|
|
let updatePublicLinkText: (String) -> Void
|
|
let displayPrivateLinkMenu: () -> Void
|
|
}
|
|
|
|
private enum ChannelVisibilitySection: Int32 {
|
|
case type
|
|
case link
|
|
case existingPublicLinks
|
|
}
|
|
|
|
private enum ChannelVisibilityEntry: ItemListNodeEntry {
|
|
case typeHeader(String)
|
|
case typePublic(Bool)
|
|
case typePrivate(Bool)
|
|
case typeInfo(String)
|
|
|
|
case privateLink(String?)
|
|
case editablePublicLink(String)
|
|
case privateLinkInfo(String)
|
|
case publicLinkInfo(String)
|
|
case publicLinkStatus(String, AddressNameStatus)
|
|
|
|
case existingLinksInfo(String)
|
|
case existingLinkPeerItem(Int32, Peer, ItemListPeerItemEditing)
|
|
|
|
var section: ItemListSectionId {
|
|
switch self {
|
|
case .typeHeader, .typePublic, .typePrivate, .typeInfo:
|
|
return ChannelVisibilitySection.type.rawValue
|
|
case .privateLink, .editablePublicLink, .privateLinkInfo, .publicLinkInfo, .publicLinkStatus:
|
|
return ChannelVisibilitySection.link.rawValue
|
|
case .existingLinksInfo, .existingLinkPeerItem:
|
|
return ChannelVisibilitySection.existingPublicLinks.rawValue
|
|
}
|
|
}
|
|
|
|
var stableId: Int32 {
|
|
switch self {
|
|
case .typeHeader:
|
|
return 0
|
|
case .typePublic:
|
|
return 1
|
|
case .typePrivate:
|
|
return 2
|
|
case .typeInfo:
|
|
return 3
|
|
|
|
case .privateLink:
|
|
return 4
|
|
case .editablePublicLink:
|
|
return 5
|
|
case .privateLinkInfo:
|
|
return 6
|
|
case .publicLinkStatus:
|
|
return 7
|
|
case .publicLinkInfo:
|
|
return 8
|
|
|
|
case .existingLinksInfo:
|
|
return 9
|
|
case let .existingLinkPeerItem(index, _, _):
|
|
return 10 + index
|
|
}
|
|
}
|
|
|
|
static func ==(lhs: ChannelVisibilityEntry, rhs: ChannelVisibilityEntry) -> Bool {
|
|
switch lhs {
|
|
case let .typeHeader(title):
|
|
if case .typeHeader(title) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .typePublic(selected):
|
|
if case .typePublic(selected) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .typePrivate(selected):
|
|
if case .typePrivate(selected) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .typeInfo(text):
|
|
if case .typeInfo(text) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .privateLink(lhsLink):
|
|
if case let .privateLink(rhsLink) = rhs, lhsLink == rhsLink {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .editablePublicLink(text):
|
|
if case .editablePublicLink(text) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .privateLinkInfo(text):
|
|
if case .privateLinkInfo(text) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .publicLinkInfo(text):
|
|
if case .publicLinkInfo(text) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .publicLinkStatus(addressName, status):
|
|
if case .publicLinkStatus(addressName, status) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .existingLinksInfo(text):
|
|
if case .existingLinksInfo(text) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .existingLinkPeerItem(lhsIndex, lhsPeer, lhsEditing):
|
|
if case let .existingLinkPeerItem(rhsIndex, rhsPeer, rhsEditing) = rhs {
|
|
if lhsIndex != rhsIndex {
|
|
return false
|
|
}
|
|
if !lhsPeer.isEqual(rhsPeer) {
|
|
return false
|
|
}
|
|
if lhsEditing != rhsEditing {
|
|
return false
|
|
}
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
static func <(lhs: ChannelVisibilityEntry, rhs: ChannelVisibilityEntry) -> Bool {
|
|
return lhs.stableId < rhs.stableId
|
|
}
|
|
|
|
func item(_ arguments: ChannelVisibilityControllerArguments) -> ListViewItem {
|
|
switch self {
|
|
case let .typeHeader(title):
|
|
return ItemListSectionHeaderItem(text: title, sectionId: self.section)
|
|
case let .typePublic(selected):
|
|
return ItemListCheckboxItem(title: "Public", checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: {
|
|
arguments.updateCurrentType(.publicChannel)
|
|
})
|
|
case let .typePrivate(selected):
|
|
return ItemListCheckboxItem(title: "Private", checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: {
|
|
arguments.updateCurrentType(.privateChannel)
|
|
})
|
|
case let .typeInfo(text):
|
|
return ItemListTextItem(text: text, sectionId: self.section)
|
|
case let .privateLink(link):
|
|
return ItemListActionItem(title: link ?? "Loading", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
|
|
|
|
})
|
|
case let .editablePublicLink(text):
|
|
return ItemListSingleLineInputItem(title: NSAttributedString(string: "t.me/", textColor: .black), text: text, placeholder: "", sectionId: self.section, textUpdated: { updatedText in
|
|
arguments.updatePublicLinkText(updatedText)
|
|
}, action: {
|
|
|
|
})
|
|
case let .privateLinkInfo(text):
|
|
return ItemListTextItem(text: text, sectionId: self.section)
|
|
case let .publicLinkInfo(text):
|
|
return ItemListTextItem(text: text, sectionId: self.section)
|
|
case let .publicLinkStatus(addressName, status):
|
|
var displayActivity = false
|
|
let text: NSAttributedString
|
|
switch status {
|
|
case .available:
|
|
text = NSAttributedString(string: "\(addressName) is available.", textColor: UIColor(0x26972c))
|
|
case .checking:
|
|
text = NSAttributedString(string: "Checking name...", textColor: .gray)
|
|
displayActivity = true
|
|
case let .invalid(reason):
|
|
switch reason {
|
|
case .alreadyTaken:
|
|
text = NSAttributedString(string: "\(addressName) is already taken.", textColor: .red)
|
|
case .digitStart:
|
|
text = NSAttributedString(string: "Names can't start with a digit.", textColor: UIColor(0xcf3030))
|
|
case .invalid, .underscopeEnd, .underscopeStart:
|
|
text = NSAttributedString(string: "Sorry, this name is invalid.", textColor: UIColor(0xcf3030))
|
|
case .short:
|
|
text = NSAttributedString(string: "Names must have at least 5 characters.", textColor: UIColor(0xcf3030))
|
|
}
|
|
}
|
|
return ItemListActivityTextItem(displayActivity: displayActivity, text: text, sectionId: self.section)
|
|
case let .existingLinksInfo(text):
|
|
return ItemListTextItem(text: text, sectionId: self.section)
|
|
case let .existingLinkPeerItem(_, peer, editing):
|
|
return ItemListPeerItem(account: arguments.account, peer: peer, presence: nil, text: .activity, label: nil, editing: editing, enabled: true, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in
|
|
|
|
}, removePeer: { _ in
|
|
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
private enum CurrentChannelType {
|
|
case publicChannel
|
|
case privateChannel
|
|
}
|
|
|
|
private enum AddressNameStatus: Equatable {
|
|
case available
|
|
case checking
|
|
case invalid(UsernameAvailabilityError)
|
|
|
|
static func ==(lhs: AddressNameStatus, rhs: AddressNameStatus) -> Bool {
|
|
switch lhs {
|
|
case .available:
|
|
if case .available = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case .checking:
|
|
if case .checking = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .invalid(reason):
|
|
if case .invalid(reason) = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct ChannelVisibilityControllerState: Equatable {
|
|
let selectedType: CurrentChannelType?
|
|
let editingPublicLinkText: String?
|
|
let addressNameStatus: AddressNameStatus?
|
|
let updatingAddressName: Bool
|
|
|
|
init() {
|
|
self.selectedType = nil
|
|
self.editingPublicLinkText = nil
|
|
self.addressNameStatus = nil
|
|
self.updatingAddressName = false
|
|
}
|
|
|
|
init(selectedType: CurrentChannelType?, editingPublicLinkText: String?, addressNameStatus: AddressNameStatus?, updatingAddressName: Bool) {
|
|
self.selectedType = selectedType
|
|
self.editingPublicLinkText = editingPublicLinkText
|
|
self.addressNameStatus = addressNameStatus
|
|
self.updatingAddressName = updatingAddressName
|
|
}
|
|
|
|
static func ==(lhs: ChannelVisibilityControllerState, rhs: ChannelVisibilityControllerState) -> Bool {
|
|
if lhs.selectedType != rhs.selectedType {
|
|
return false
|
|
}
|
|
if lhs.editingPublicLinkText != rhs.editingPublicLinkText {
|
|
return false
|
|
}
|
|
if lhs.addressNameStatus != rhs.addressNameStatus {
|
|
return false
|
|
}
|
|
if lhs.updatingAddressName != rhs.updatingAddressName {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func withUpdatedSelectedType(_ selectedType: CurrentChannelType?) -> ChannelVisibilityControllerState {
|
|
return ChannelVisibilityControllerState(selectedType: selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameStatus: self.addressNameStatus, updatingAddressName: self.updatingAddressName)
|
|
}
|
|
|
|
func withUpdatedEditingPublicLinkText(_ editingPublicLinkText: String?) -> ChannelVisibilityControllerState {
|
|
return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: editingPublicLinkText, addressNameStatus: self.addressNameStatus, updatingAddressName: self.updatingAddressName)
|
|
}
|
|
|
|
func withUpdatedAddressNameStatus(_ addressNameStatus: AddressNameStatus?) -> ChannelVisibilityControllerState {
|
|
return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameStatus: addressNameStatus, updatingAddressName: self.updatingAddressName)
|
|
}
|
|
|
|
func withUpdatedUpdatingAddressName(_ updatingAddressName: Bool) -> ChannelVisibilityControllerState {
|
|
return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameStatus: self.addressNameStatus, updatingAddressName: updatingAddressName)
|
|
}
|
|
}
|
|
|
|
private func channelVisibilityControllerEntries(view: PeerView, state: ChannelVisibilityControllerState) -> [ChannelVisibilityEntry] {
|
|
var entries: [ChannelVisibilityEntry] = []
|
|
|
|
if let peer = view.peers[view.peerId] as? TelegramChannel {
|
|
var isGroup = false
|
|
if case .group = peer.info {
|
|
isGroup = true
|
|
}
|
|
|
|
let selectedType: CurrentChannelType
|
|
if let current = state.selectedType {
|
|
selectedType = current
|
|
} else {
|
|
if let addressName = peer.addressName, !addressName.isEmpty {
|
|
selectedType = .publicChannel
|
|
} else {
|
|
selectedType = .privateChannel
|
|
}
|
|
}
|
|
|
|
let currentAddressName: String
|
|
if let current = state.editingPublicLinkText {
|
|
currentAddressName = current
|
|
} else {
|
|
if let addressName = peer.addressName {
|
|
currentAddressName = addressName
|
|
} else {
|
|
currentAddressName = ""
|
|
}
|
|
}
|
|
|
|
entries.append(.typeHeader(isGroup ? "GROUP TYPE" : "CHANNEL TYPE"))
|
|
entries.append(.typePublic(selectedType == .publicChannel))
|
|
entries.append(.typePrivate(selectedType == .privateChannel))
|
|
|
|
switch selectedType {
|
|
case .publicChannel:
|
|
if isGroup {
|
|
entries.append(.typeInfo("Public groups can be found in search, chat history is available to everyone and anyone can join."))
|
|
} else {
|
|
entries.append(.typeInfo("Public channels can be found in search and anyone can join."))
|
|
}
|
|
case .privateChannel:
|
|
if isGroup {
|
|
entries.append(.typeInfo("Private groups can only be joined if you were invited of have an invite link."))
|
|
} else {
|
|
entries.append(.typeInfo("Private channels can only be joined if you were invited of have an invite link."))
|
|
}
|
|
}
|
|
|
|
switch selectedType {
|
|
case .publicChannel:
|
|
entries.append(.editablePublicLink(currentAddressName))
|
|
if let status = state.addressNameStatus {
|
|
entries.append(.publicLinkStatus(currentAddressName, status))
|
|
}
|
|
entries.append(.publicLinkInfo("People can share this link with others and find your group using Telegram search."))
|
|
case .privateChannel:
|
|
entries.append(.privateLink((view.cachedData as? CachedChannelData)?.exportedInvitation?.link))
|
|
entries.append(.publicLinkInfo("People can join your group by following this link. You can revoke the link at any time."))
|
|
}
|
|
}
|
|
|
|
return entries
|
|
}
|
|
private func effectiveChannelType(state: ChannelVisibilityControllerState, peer: TelegramChannel) -> CurrentChannelType {
|
|
let selectedType: CurrentChannelType
|
|
if let current = state.selectedType {
|
|
selectedType = current
|
|
} else {
|
|
if let addressName = peer.addressName, !addressName.isEmpty {
|
|
selectedType = .publicChannel
|
|
} else {
|
|
selectedType = .privateChannel
|
|
}
|
|
}
|
|
return selectedType
|
|
}
|
|
|
|
private func updatedAddressName(state: ChannelVisibilityControllerState, peer: TelegramChannel) -> String? {
|
|
let selectedType = effectiveChannelType(state: state, peer: peer)
|
|
|
|
let currentAddressName: String
|
|
|
|
switch selectedType {
|
|
case .privateChannel:
|
|
currentAddressName = ""
|
|
case .publicChannel:
|
|
if let current = state.editingPublicLinkText {
|
|
currentAddressName = current
|
|
} else {
|
|
if let addressName = peer.addressName {
|
|
currentAddressName = addressName
|
|
} else {
|
|
currentAddressName = ""
|
|
}
|
|
}
|
|
}
|
|
|
|
if !currentAddressName.isEmpty {
|
|
if currentAddressName != peer.addressName {
|
|
return currentAddressName
|
|
} else {
|
|
return nil
|
|
}
|
|
} else if peer.addressName != nil {
|
|
return ""
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
public func channelVisibilityController(account: Account, peerId: PeerId) -> ViewController {
|
|
let statePromise = ValuePromise(ChannelVisibilityControllerState(), ignoreRepeated: true)
|
|
let stateValue = Atomic(value: ChannelVisibilityControllerState())
|
|
let updateState: ((ChannelVisibilityControllerState) -> ChannelVisibilityControllerState) -> Void = { f in
|
|
statePromise.set(stateValue.modify { f($0) })
|
|
}
|
|
|
|
var dismissImpl: (() -> Void)?
|
|
|
|
let actionsDisposable = DisposableSet()
|
|
|
|
let checkAddressNameDisposable = MetaDisposable()
|
|
actionsDisposable.add(checkAddressNameDisposable)
|
|
|
|
let updateAddressNameDisposable = MetaDisposable()
|
|
actionsDisposable.add(updateAddressNameDisposable)
|
|
|
|
let arguments = ChannelVisibilityControllerArguments(account: account, updateCurrentType: { type in
|
|
updateState { state in
|
|
return state.withUpdatedSelectedType(type)
|
|
}
|
|
}, updatePublicLinkText: { text in
|
|
if text.isEmpty {
|
|
checkAddressNameDisposable.set(nil)
|
|
updateState { state in
|
|
return state.withUpdatedEditingPublicLinkText(text).withUpdatedAddressNameStatus(nil)
|
|
}
|
|
} else {
|
|
updateState { state in
|
|
return state.withUpdatedEditingPublicLinkText(text)
|
|
}
|
|
checkAddressNameDisposable.set((addressNameAvailability(account: account, domain: .peer(peerId), def: nil, current: text)
|
|
|> deliverOnMainQueue).start(next: { result in
|
|
updateState { state in
|
|
let status: AddressNameStatus
|
|
switch result {
|
|
case let .fail(_, error):
|
|
status = .invalid(error)
|
|
case .none:
|
|
status = .available
|
|
case .success:
|
|
status = .available
|
|
case .progress:
|
|
status = .checking
|
|
}
|
|
return state.withUpdatedAddressNameStatus(status)
|
|
}
|
|
}))
|
|
}
|
|
}, displayPrivateLinkMenu: {
|
|
|
|
})
|
|
|
|
let peerView = account.viewTracker.peerView(peerId)
|
|
|
|
let signal = combineLatest(statePromise.get(), peerView)
|
|
|> map { state, view -> (ItemListControllerState, (ItemListNodeState<ChannelVisibilityEntry>, ChannelVisibilityEntry.ItemGenerationArguments)) in
|
|
let peer = peerViewMainPeer(view)
|
|
|
|
var rightNavigationButton: ItemListNavigationButton?
|
|
if let peer = peer as? TelegramChannel {
|
|
var doneEnabled = true
|
|
if let selectedType = state.selectedType {
|
|
switch selectedType {
|
|
case .privateChannel:
|
|
break
|
|
case .publicChannel:
|
|
if let addressNameStatus = state.addressNameStatus {
|
|
switch addressNameStatus {
|
|
case .available:
|
|
break
|
|
default:
|
|
doneEnabled = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
rightNavigationButton = ItemListNavigationButton(title: "Done", style: state.updatingAddressName ? .activity : .bold, enabled: doneEnabled, action: {
|
|
var updatedAddressNameValue: String?
|
|
updateState { state in
|
|
updatedAddressNameValue = updatedAddressName(state: state, peer: peer)
|
|
|
|
if updatedAddressNameValue != nil {
|
|
return state.withUpdatedUpdatingAddressName(true)
|
|
} else {
|
|
return state
|
|
}
|
|
}
|
|
|
|
if let updatedAddressNameValue = updatedAddressNameValue {
|
|
updateAddressNameDisposable.set((updatePeerAddressName(account: account, peerId: peerId, username: updatedAddressNameValue.isEmpty ? nil : updatedAddressNameValue)
|
|
|> deliverOnMainQueue).start(error: { _ in
|
|
updateState { state in
|
|
return state.withUpdatedUpdatingAddressName(false)
|
|
}
|
|
}, completed: {
|
|
updateState { state in
|
|
return state.withUpdatedUpdatingAddressName(false)
|
|
}
|
|
|
|
dismissImpl?()
|
|
}))
|
|
} else {
|
|
dismissImpl?()
|
|
}
|
|
})
|
|
}
|
|
|
|
var isGroup = false
|
|
if let peer = peer as? TelegramChannel {
|
|
if case .group = peer.info {
|
|
isGroup = true
|
|
}
|
|
}
|
|
|
|
let leftNavigationButton = ItemListNavigationButton(title: "Cancel", style: .regular, enabled: true, action: {
|
|
dismissImpl?()
|
|
})
|
|
|
|
let controllerState = ItemListControllerState(title: isGroup ? "Group Type" : "Channel Link", leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, animateChanges: false)
|
|
let listState = ItemListNodeState(entries: channelVisibilityControllerEntries(view: view, state: state), style: .blocks, animateChanges: false)
|
|
|
|
return (controllerState, (listState, arguments))
|
|
} |> afterDisposed {
|
|
actionsDisposable.dispose()
|
|
}
|
|
|
|
let controller = ItemListController(signal)
|
|
dismissImpl = { [weak controller] in
|
|
controller?.dismiss()
|
|
}
|
|
return controller
|
|
}
|