My Profile improvements

This commit is contained in:
Isaac 2024-04-08 17:04:46 +04:00
parent 53f6799f98
commit 8dc59be281
12 changed files with 702 additions and 156 deletions

View File

@ -316,7 +316,7 @@ public class ItemListAddressItemNode: ListViewItemNode {
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
strongSelf.topStripeNode.isHidden = !item.displayDecorations
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners || !item.displayDecorations
@ -327,12 +327,12 @@ public class ItemListAddressItemNode: ListViewItemNode {
case .sameSection(false):
bottomStripeInset = 16.0 + params.leftInset
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
strongSelf.bottomStripeNode.isHidden = !item.displayDecorations
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
strongSelf.bottomStripeNode.isHidden = hasCorners || !item.displayDecorations
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil

View File

@ -175,7 +175,7 @@ public enum AccountResult {
case authorized(Account)
}
public func accountWithId(accountManager: AccountManager<TelegramAccountManagerTypes>, networkArguments: NetworkInitializationArguments, id: AccountRecordId, encryptionParameters: ValueBoxEncryptionParameters, supplementary: Bool, rootPath: String, beginWithTestingEnvironment: Bool, backupData: AccountBackupData?, auxiliaryMethods: AccountAuxiliaryMethods, shouldKeepAutoConnection: Bool = true) -> Signal<AccountResult, NoError> {
public func accountWithId(accountManager: AccountManager<TelegramAccountManagerTypes>, networkArguments: NetworkInitializationArguments, id: AccountRecordId, encryptionParameters: ValueBoxEncryptionParameters, supplementary: Bool, isSupportUser: Bool, rootPath: String, beginWithTestingEnvironment: Bool, backupData: AccountBackupData?, auxiliaryMethods: AccountAuxiliaryMethods, shouldKeepAutoConnection: Bool = true) -> Signal<AccountResult, NoError> {
let path = "\(rootPath)/\(accountRecordIdPathName(id))"
let postbox = openPostbox(
@ -259,7 +259,7 @@ public func accountWithId(accountManager: AccountManager<TelegramAccountManagerT
|> mapToSignal { phoneNumber in
return initializedNetwork(accountId: id, arguments: networkArguments, supplementary: supplementary, datacenterId: Int(authorizedState.masterDatacenterId), keychain: keychain, basePath: path, testingEnvironment: authorizedState.isTestingEnvironment, languageCode: localizationSettings?.primaryComponent.languageCode, proxySettings: proxySettings, networkSettings: networkSettings, phoneNumber: phoneNumber, useRequestTimeoutTimers: useRequestTimeoutTimers, appConfiguration: appConfig)
|> map { network -> AccountResult in
return .authorized(Account(accountManager: accountManager, id: id, basePath: path, testingEnvironment: authorizedState.isTestingEnvironment, postbox: postbox, network: network, networkArguments: networkArguments, peerId: authorizedState.peerId, auxiliaryMethods: auxiliaryMethods, supplementary: supplementary))
return .authorized(Account(accountManager: accountManager, id: id, basePath: path, testingEnvironment: authorizedState.isTestingEnvironment, postbox: postbox, network: network, networkArguments: networkArguments, peerId: authorizedState.peerId, auxiliaryMethods: auxiliaryMethods, supplementary: supplementary, isSupportUser: isSupportUser))
}
}
case _:
@ -905,6 +905,7 @@ public class Account {
public let basePath: String
public let testingEnvironment: Bool
public let supplementary: Bool
public let isSupportUser: Bool
public let postbox: Postbox
public let network: Network
public let networkArguments: NetworkInitializationArguments
@ -988,7 +989,7 @@ public class Account {
public let filteredStorySubscriptionsContext: StorySubscriptionsContext?
public let hiddenStorySubscriptionsContext: StorySubscriptionsContext?
public init(accountManager: AccountManager<TelegramAccountManagerTypes>, id: AccountRecordId, basePath: String, testingEnvironment: Bool, postbox: Postbox, network: Network, networkArguments: NetworkInitializationArguments, peerId: PeerId, auxiliaryMethods: AccountAuxiliaryMethods, supplementary: Bool) {
public init(accountManager: AccountManager<TelegramAccountManagerTypes>, id: AccountRecordId, basePath: String, testingEnvironment: Bool, postbox: Postbox, network: Network, networkArguments: NetworkInitializationArguments, peerId: PeerId, auxiliaryMethods: AccountAuxiliaryMethods, supplementary: Bool, isSupportUser: Bool) {
self.accountManager = accountManager
self.id = id
self.basePath = basePath
@ -1000,6 +1001,7 @@ public class Account {
self.auxiliaryMethods = auxiliaryMethods
self.supplementary = supplementary
self.isSupportUser = isSupportUser
self.networkStatsContext = NetworkStatsContext(postbox: postbox)

View File

@ -9,12 +9,18 @@ private enum AccountKind {
case unauthorized
}
public struct AccountSupportUserInfo: Codable, Equatable {
public init() {
}
}
public enum TelegramAccountRecordAttribute: AccountRecordAttribute, Equatable {
enum CodingKeys: String, CodingKey {
case backupData
case environment
case sortOrder
case loggedOut
case supportUserInfo
case legacyRootObject = "_"
}
@ -22,6 +28,7 @@ public enum TelegramAccountRecordAttribute: AccountRecordAttribute, Equatable {
case environment(AccountEnvironmentAttribute)
case sortOrder(AccountSortOrderAttribute)
case loggedOut(LoggedOutAccountAttribute)
case supportUserInfo(AccountSupportUserInfo)
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
@ -34,6 +41,8 @@ public enum TelegramAccountRecordAttribute: AccountRecordAttribute, Equatable {
self = .sortOrder(sortOrder)
} else if let loggedOut = try? container.decodeIfPresent(LoggedOutAccountAttribute.self, forKey: .loggedOut) {
self = .loggedOut(loggedOut)
} else if let supportUserInfo = try? container.decodeIfPresent(AccountSupportUserInfo.self, forKey: .supportUserInfo) {
self = .supportUserInfo(supportUserInfo)
} else {
let legacyRootObjectData = try! container.decode(AdaptedPostboxDecoder.RawObjectData.self, forKey: .legacyRootObject)
if legacyRootObjectData.typeHash == postboxEncodableTypeHash(AccountBackupDataAttribute.self) {
@ -62,6 +71,8 @@ public enum TelegramAccountRecordAttribute: AccountRecordAttribute, Equatable {
try container.encode(sortOrder, forKey: .sortOrder)
case let .loggedOut(loggedOut):
try container.encode(loggedOut, forKey: .loggedOut)
case let .supportUserInfo(supportUserInfo):
try container.encode(supportUserInfo, forKey: .supportUserInfo)
}
}
@ -271,7 +282,14 @@ public func currentAccount(allocateIfNotExists: Bool, networkArguments: NetworkI
return false
}
})
return accountWithId(accountManager: manager, networkArguments: networkArguments, id: record.0, encryptionParameters: encryptionParameters, supplementary: supplementary, rootPath: rootPath, beginWithTestingEnvironment: beginWithTestingEnvironment, backupData: nil, auxiliaryMethods: auxiliaryMethods)
let isSupportUser = record.1.contains(where: { attribute in
if case .supportUserInfo = attribute {
return true
} else {
return false
}
})
return accountWithId(accountManager: manager, networkArguments: networkArguments, id: record.0, encryptionParameters: encryptionParameters, supplementary: supplementary, isSupportUser: isSupportUser, rootPath: rootPath, beginWithTestingEnvironment: beginWithTestingEnvironment, backupData: nil, auxiliaryMethods: auxiliaryMethods)
|> mapToSignal { accountResult -> Signal<AccountResult?, NoError> in
let postbox: Postbox
let initialKind: AccountKind
@ -441,7 +459,14 @@ private func cleanupAccount(networkArguments: NetworkInitializationArguments, ac
return false
}
})
return accountWithId(accountManager: accountManager, networkArguments: networkArguments, id: id, encryptionParameters: encryptionParameters, supplementary: true, rootPath: rootPath, beginWithTestingEnvironment: beginWithTestingEnvironment, backupData: nil, auxiliaryMethods: auxiliaryMethods)
let isSupportUser = attributes.contains(where: { attribute in
if case .supportUserInfo = attribute {
return true
} else {
return false
}
})
return accountWithId(accountManager: accountManager, networkArguments: networkArguments, id: id, encryptionParameters: encryptionParameters, supplementary: true, isSupportUser: isSupportUser, rootPath: rootPath, beginWithTestingEnvironment: beginWithTestingEnvironment, backupData: nil, auxiliaryMethods: auxiliaryMethods)
|> mapToSignal { account -> Signal<Void, NoError> in
switch account {
case .upgrading:

View File

@ -15,7 +15,7 @@ public enum AuthorizationCodeRequestError {
case appOutdated
}
func switchToAuthorizedAccount(transaction: AccountManagerModifier<TelegramAccountManagerTypes>, account: UnauthorizedAccount) {
func switchToAuthorizedAccount(transaction: AccountManagerModifier<TelegramAccountManagerTypes>, account: UnauthorizedAccount, isSupportUser: Bool) {
let nextSortOrder = (transaction.getRecords().map({ record -> Int32 in
for attribute in record.attributes {
if case let .sortOrder(sortOrder) = attribute {
@ -25,10 +25,14 @@ func switchToAuthorizedAccount(transaction: AccountManagerModifier<TelegramAccou
return 0
}).max() ?? 0) + 1
transaction.updateRecord(account.id, { _ in
return AccountRecord(id: account.id, attributes: [
var attributes: [TelegramAccountManagerTypes.Attribute] = [
.environment(AccountEnvironmentAttribute(environment: account.testingEnvironment ? .test : .production)),
.sortOrder(AccountSortOrderAttribute(order: nextSortOrder))
], temporarySessionId: nil)
]
if isSupportUser {
attributes.append(.supportUserInfo(AccountSupportUserInfo()))
}
return AccountRecord(id: account.id, attributes: attributes, temporarySessionId: nil)
})
transaction.setCurrentId(account.id)
transaction.removeAuth()
@ -323,6 +327,10 @@ public func sendAuthorizationCode(accountManager: AccountManager<TelegramAccount
}
let user = TelegramUser(user: user)
var isSupportUser = false
if let phone = user.phone, phone.hasPrefix("42") {
isSupportUser = true
}
let state = AuthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil, invalidatedChannels: [])
initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts)
transaction.setState(state)
@ -330,7 +338,7 @@ public func sendAuthorizationCode(accountManager: AccountManager<TelegramAccount
transaction.setNoticeEntry(key: value.0, value: value.1)
}
return accountManager.transaction { transaction -> SendAuthorizationCodeResult in
switchToAuthorizedAccount(transaction: transaction, account: account)
switchToAuthorizedAccount(transaction: transaction, account: account, isSupportUser: isSupportUser)
return .loggedIn
}
|> castError(AuthorizationCodeRequestError.self)
@ -951,6 +959,10 @@ public func authorizeWithCode(accountManager: AccountManager<TelegramAccountMana
}
let user = TelegramUser(user: user)
var isSupportUser = false
if let phone = user.phone, phone.hasPrefix("42") {
isSupportUser = true
}
let state = AuthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil, invalidatedChannels: [])
initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts)
transaction.setState(state)
@ -958,7 +970,7 @@ public func authorizeWithCode(accountManager: AccountManager<TelegramAccountMana
transaction.setNoticeEntry(key: value.0, value: value.1)
}
return accountManager.transaction { transaction -> AuthorizeWithCodeResult in
switchToAuthorizedAccount(transaction: transaction, account: account)
switchToAuthorizedAccount(transaction: transaction, account: account, isSupportUser: isSupportUser)
return .loggedIn
}
case let .authorizationSignUpRequired(_, termsOfService):
@ -1019,11 +1031,15 @@ public func authorizeWithPassword(accountManager: AccountManager<TelegramAccount
/*transaction.updatePeersInternal([user], update: { current, peer -> Peer? in
return peer
})*/
var isSupportUser = false
if let phone = user.phone, phone.hasPrefix("42") {
isSupportUser = true
}
initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts)
transaction.setState(state)
return accountManager.transaction { transaction -> Void in
switchToAuthorizedAccount(transaction: transaction, account: account)
switchToAuthorizedAccount(transaction: transaction, account: account, isSupportUser: isSupportUser)
}
case .authorizationSignUpRequired:
return .complete()
@ -1085,12 +1101,16 @@ public func loginWithRecoveredAccountData(accountManager: AccountManager<Telegra
}
let user = TelegramUser(user: user)
var isSupportUser = false
if let phone = user.phone, phone.hasPrefix("42") {
isSupportUser = true
}
let state = AuthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil, invalidatedChannels: [])
initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts)
transaction.setState(state)
return accountManager.transaction { transaction -> Void in
switchToAuthorizedAccount(transaction: transaction, account: account)
switchToAuthorizedAccount(transaction: transaction, account: account, isSupportUser: isSupportUser)
}
case .authorizationSignUpRequired:
return .complete()
@ -1237,6 +1257,10 @@ public func signUpWithName(accountManager: AccountManager<TelegramAccountManager
}
let user = TelegramUser(user: user)
var isSupportUser = false
if let phone = user.phone, phone.hasPrefix("42") {
isSupportUser = true
}
let appliedState = account.postbox.transaction { transaction -> Void in
let state = AuthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil, invalidatedChannels: [])
if let hole = account.postbox.seedConfiguration.initializeChatListWithHole.topLevel {
@ -1251,7 +1275,7 @@ public func signUpWithName(accountManager: AccountManager<TelegramAccountManager
|> castError(SignUpError.self)
let switchedAccounts = accountManager.transaction { transaction -> Void in
switchToAuthorizedAccount(transaction: transaction, account: account)
switchToAuthorizedAccount(transaction: transaction, account: account, isSupportUser: isSupportUser)
}
|> castError(SignUpError.self)

View File

@ -435,6 +435,22 @@ public func resendMessages(account: Account, messageIds: [MessageId]) -> Signal<
}
func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, messages: [(Bool, EnqueueMessage)], disableAutoremove: Bool = false, transformGroupingKeysWithPeerId: Bool = false) -> [MessageId?] {
/**
* If it is a support account, mark messages as read here as they are
* not marked as read when chat is opened.
**/
if account.isSupportUser {
let namespace: MessageId.Namespace
if peerId.namespace == Namespaces.Peer.SecretChat {
namespace = Namespaces.Message.SecretIncoming
} else {
namespace = Namespaces.Message.Cloud
}
if let index = transaction.getTopPeerMessageIndex(peerId: peerId, namespace: namespace) {
let _ = transaction.applyInteractiveReadMaxIndex(index)
}
}
var forwardedMessageIds = Set<MessageId>()
for (_, message) in messages {
if case let .forward(sourceId, _, _, _, _) = message {

View File

@ -103,7 +103,7 @@ func _internal_exportAuthTransferToken(accountManager: AccountManager<TelegramAc
initializedAppSettingsAfterLogin(transaction: transaction, appVersion: updatedAccount.networkArguments.appVersion, syncContacts: syncContacts)
transaction.setState(state)
return accountManager.transaction { transaction -> ExportAuthTransferTokenResult in
switchToAuthorizedAccount(transaction: transaction, account: updatedAccount)
switchToAuthorizedAccount(transaction: transaction, account: updatedAccount, isSupportUser: false)
return .loggedIn
}
|> castError(ExportAuthTransferTokenError.self)
@ -131,7 +131,7 @@ func _internal_exportAuthTransferToken(accountManager: AccountManager<TelegramAc
initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts)
transaction.setState(state)
return accountManager.transaction { transaction -> ExportAuthTransferTokenResult in
switchToAuthorizedAccount(transaction: transaction, account: account)
switchToAuthorizedAccount(transaction: transaction, account: account, isSupportUser: false)
return .loggedIn
}
|> castError(ExportAuthTransferTokenError.self)

View File

@ -15,6 +15,7 @@ final class PeerInfoScreenAddressItem: PeerInfoScreenItem {
let action: (() -> Void)?
let longTapAction: ((ASDisplayNode, String) -> Void)?
let linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)?
let contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?
init(
id: AnyHashable,
@ -23,7 +24,8 @@ final class PeerInfoScreenAddressItem: PeerInfoScreenItem {
imageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?,
action: (() -> Void)?,
longTapAction: ((ASDisplayNode, String) -> Void)? = nil,
linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil
linkItemAction: ((TextLinkItemActionType, TextLinkItem) -> Void)? = nil,
contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? = nil
) {
self.id = id
self.label = label
@ -32,6 +34,7 @@ final class PeerInfoScreenAddressItem: PeerInfoScreenItem {
self.action = action
self.longTapAction = longTapAction
self.linkItemAction = linkItemAction
self.contextAction = contextAction
}
func node() -> PeerInfoScreenItemNode {
@ -40,41 +43,130 @@ final class PeerInfoScreenAddressItem: PeerInfoScreenItem {
}
private final class PeerInfoScreenAddressItemNode: PeerInfoScreenItemNode {
private let selectionNode: PeerInfoScreenSelectableBackgroundNode
private let bottomSeparatorNode: ASDisplayNode
private let containerNode: ContextControllerSourceNode
private let contextSourceNode: ContextExtractedContentContainingNode
private let extractedBackgroundImageNode: ASImageNode
private var extractedRect: CGRect?
private var nonExtractedRect: CGRect?
private let maskNode: ASImageNode
private let bottomSeparatorNode: ASDisplayNode
private let activateArea: AccessibilityAreaNode
private var item: PeerInfoScreenAddressItem?
private var itemNode: ItemListAddressItemNode?
private var presentationData: PresentationData?
override init() {
var bringToFrontForHighlightImpl: (() -> Void)?
self.selectionNode = PeerInfoScreenSelectableBackgroundNode(bringToFrontForHighlight: { bringToFrontForHighlightImpl?() })
self.contextSourceNode = ContextExtractedContentContainingNode()
self.containerNode = ContextControllerSourceNode()
self.bottomSeparatorNode = ASDisplayNode()
self.bottomSeparatorNode.isLayerBacked = true
self.extractedBackgroundImageNode = ASImageNode()
self.extractedBackgroundImageNode.displaysAsynchronously = false
self.extractedBackgroundImageNode.alpha = 0.0
self.maskNode = ASImageNode()
self.maskNode.isUserInteractionEnabled = false
self.bottomSeparatorNode = ASDisplayNode()
self.bottomSeparatorNode.isLayerBacked = true
self.activateArea = AccessibilityAreaNode()
super.init()
bringToFrontForHighlightImpl = { [weak self] in
self?.bringToFrontForHighlight?()
}
self.addSubnode(self.bottomSeparatorNode)
self.addSubnode(self.selectionNode)
self.containerNode.addSubnode(self.contextSourceNode)
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
self.addSubnode(self.containerNode)
self.addSubnode(self.maskNode)
self.view.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(self.longPressGesture(_:))))
self.contextSourceNode.contentNode.clipsToBounds = true
self.contextSourceNode.contentNode.addSubnode(self.extractedBackgroundImageNode)
self.addSubnode(self.activateArea)
self.containerNode.isGestureEnabled = false
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self, let item = strongSelf.item, let contextAction = item.contextAction else {
gesture.cancel()
return
}
contextAction(strongSelf.contextSourceNode, gesture, nil)
}
self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
guard let strongSelf = self, let presentationData = strongSelf.presentationData else {
return
}
let theme = presentationData.theme
if isExtracted {
strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: theme.list.plainBackgroundColor)
}
if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect {
let rect = isExtracted ? extractedRect : nonExtractedRect
transition.updateFrame(node: strongSelf.extractedBackgroundImageNode, frame: rect)
}
transition.updateAlpha(node: strongSelf.extractedBackgroundImageNode, alpha: isExtracted ? 1.0 : 0.0, completion: { _ in
if !isExtracted {
self?.extractedBackgroundImageNode.image = nil
}
})
}
}
@objc private func longPressGesture(_ recognizer: UILongPressGestureRecognizer) {
if case .began = recognizer.state {
if let item = self.item {
item.longTapAction?(self, item.text)
override func didLoad() {
super.didLoad()
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
recognizer.tapActionAtPoint = { [weak self] _ in
guard let self, let item = self.item else {
return .keepWithSingleTap
}
if item.longTapAction != nil {
return .waitForSingleTap
}
return .waitForSingleTap
}
recognizer.highlight = { [weak self] point in
guard let strongSelf = self else {
return
}
strongSelf.updateTouchesAtPoint(point)
}
self.view.addGestureRecognizer(recognizer)
}
@objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
if let item = self.item {
item.action?()
}
case .longTap:
if let item = self.item {
item.longTapAction?(self, item.text)
}
default:
break
}
}
default:
break
}
}
@ -84,9 +176,10 @@ private final class PeerInfoScreenAddressItemNode: PeerInfoScreenItemNode {
}
self.item = item
self.presentationData = presentationData
self.selectionNode.pressed = item.action
self.containerNode.isGestureEnabled = item.contextAction != nil
let sideInset: CGFloat = 16.0 + safeInsets.left
self.bottomSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor
@ -100,7 +193,7 @@ private final class PeerInfoScreenAddressItemNode: PeerInfoScreenItemNode {
itemNode = current
addressItem.updateNode(async: { $0() }, node: {
return itemNode
}, params: params, previousItem: topItem != nil ? addressItem : nil, nextItem: bottomItem != nil ? addressItem : nil, animation: .None, completion: { (layout, apply) in
}, params: params, previousItem: addressItem, nextItem: addressItem, animation: .None, completion: { (layout, apply) in
let nodeFrame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: layout.size.height))
itemNode.contentSize = layout.contentSize
@ -118,28 +211,45 @@ private final class PeerInfoScreenAddressItemNode: PeerInfoScreenItemNode {
itemNode = itemNodeValue as! ItemListAddressItemNode
itemNode.isUserInteractionEnabled = false
self.itemNode = itemNode
self.addSubnode(itemNode)
self.contextSourceNode.contentNode.addSubnode(itemNode)
}
let height = itemNode.contentSize.height
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel)))
transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0)
let hasCorners = hasCorners && (topItem == nil || bottomItem == nil)
let hasTopCorners = hasCorners && topItem == nil
let hasBottomCorners = hasCorners && bottomItem == nil
self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
self.maskNode.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height))
self.bottomSeparatorNode.isHidden = hasBottomCorners || bottomItem != nil
transition.updateFrame(node: self.maskNode, frame: CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height)))
self.bottomSeparatorNode.isHidden = hasBottomCorners
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(), size: itemNode.bounds.size))
self.activateArea.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: height))
self.activateArea.accessibilityLabel = item.label
let highlightNodeOffset: CGFloat = topItem == nil ? 0.0 : UIScreenPixel
self.selectionNode.update(size: CGSize(width: width, height: height + highlightNodeOffset), theme: presentationData.theme, transition: transition)
transition.updateFrame(node: self.selectionNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -highlightNodeOffset), size: CGSize(width: width, height: height + highlightNodeOffset)))
let contentSize = CGSize(width: width, height: height)
self.containerNode.frame = CGRect(origin: CGPoint(), size: contentSize)
self.contextSourceNode.frame = CGRect(origin: CGPoint(), size: contentSize)
transition.updateFrame(node: self.contextSourceNode.contentNode, frame: CGRect(origin: CGPoint(), size: contentSize))
transition.updateFrame(node: self.bottomSeparatorNode, frame: CGRect(origin: CGPoint(x: sideInset, y: height - UIScreenPixel), size: CGSize(width: width - sideInset, height: UIScreenPixel)))
transition.updateAlpha(node: self.bottomSeparatorNode, alpha: bottomItem == nil ? 0.0 : 1.0)
let nonExtractedRect = CGRect(origin: CGPoint(), size: CGSize(width: contentSize.width, height: contentSize.height))
let extractedRect = nonExtractedRect
self.extractedRect = extractedRect
self.nonExtractedRect = nonExtractedRect
if self.contextSourceNode.isExtractedToContextPreview {
self.extractedBackgroundImageNode.frame = extractedRect
} else {
self.extractedBackgroundImageNode.frame = nonExtractedRect
}
self.contextSourceNode.contentRect = extractedRect
return height
}
private func updateTouchesAtPoint(_ point: CGPoint?) {
}
}

View File

@ -13,6 +13,54 @@ import MultilineTextComponent
import BundleIconComponent
import PlainButtonComponent
func businessHoursTextToCopy(businessHours: TelegramBusinessHours, presentationData: PresentationData, displayLocalTimezone: Bool) -> String {
var text = ""
let cachedDays = businessHours.splitIntoWeekDays()
var timezoneOffsetMinutes: Int = 0
if displayLocalTimezone {
var currentCalendar = Calendar(identifier: .gregorian)
currentCalendar.timeZone = TimeZone(identifier: businessHours.timezoneId) ?? TimeZone.current
let currentTimezone = TimeZone.current
timezoneOffsetMinutes = (currentTimezone.secondsFromGMT() - currentCalendar.timeZone.secondsFromGMT()) / 60
}
let businessDays: [TelegramBusinessHours.WeekDay] = cachedDays
for i in 0 ..< businessDays.count {
let dayTitleValue: String
switch i {
case 0:
dayTitleValue = presentationData.strings.Weekday_Monday
case 1:
dayTitleValue = presentationData.strings.Weekday_Tuesday
case 2:
dayTitleValue = presentationData.strings.Weekday_Wednesday
case 3:
dayTitleValue = presentationData.strings.Weekday_Thursday
case 4:
dayTitleValue = presentationData.strings.Weekday_Friday
case 5:
dayTitleValue = presentationData.strings.Weekday_Saturday
case 6:
dayTitleValue = presentationData.strings.Weekday_Sunday
default:
dayTitleValue = " "
}
let businessHoursText = dayBusinessHoursText(presentationData: presentationData, day: businessDays[i], offsetMinutes: timezoneOffsetMinutes, formatAsPlainText: true)
if !text.isEmpty {
text.append("\n")
}
text.append("\(dayTitleValue): \(businessHoursText)")
}
return text
}
private func dayBusinessHoursText(presentationData: PresentationData, day: TelegramBusinessHours.WeekDay, offsetMinutes: Int, formatAsPlainText: Bool = false) -> String {
var businessHoursText: String = ""
switch day {
@ -59,20 +107,23 @@ final class PeerInfoScreenBusinessHoursItem: PeerInfoScreenItem {
let label: String
let businessHours: TelegramBusinessHours
let requestLayout: (Bool) -> Void
let longTapAction: (ASDisplayNode, String) -> Void
let longTapAction: ((ASDisplayNode, String) -> Void)?
let contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?
init(
id: AnyHashable,
label: String,
businessHours: TelegramBusinessHours,
requestLayout: @escaping (Bool) -> Void,
longTapAction: @escaping (ASDisplayNode, String) -> Void
longTapAction: ((ASDisplayNode, String) -> Void)? = nil,
contextAction: ((ASDisplayNode, ContextGesture?, CGPoint?) -> Void)? = nil
) {
self.id = id
self.label = label
self.businessHours = businessHours
self.requestLayout = requestLayout
self.longTapAction = longTapAction
self.contextAction = contextAction
}
func node() -> PeerInfoScreenItemNode {
@ -154,6 +205,14 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode
self.containerNode.isGestureEnabled = false
self.containerNode.activated = { [weak self] gesture, _ in
guard let strongSelf = self, let item = strongSelf.item, let contextAction = item.contextAction else {
gesture.cancel()
return
}
contextAction(strongSelf.contextSourceNode, gesture, nil)
}
self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
guard let strongSelf = self, let theme = strongSelf.theme else {
return
@ -180,7 +239,14 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode
super.didLoad()
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
recognizer.tapActionAtPoint = { _ in
recognizer.tapActionAtPoint = { [weak self] _ in
guard let self, let item = self.item else {
return .keepWithSingleTap
}
if item.longTapAction != nil {
return .waitForSingleTap
}
return .waitForSingleTap
}
recognizer.highlight = { [weak self] point in
@ -202,48 +268,7 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode
self.item?.requestLayout(true)
case .longTap:
if let item = self.item, let presentationData = self.presentationData {
var text = ""
var timezoneOffsetMinutes: Int = 0
if self.displayLocalTimezone {
var currentCalendar = Calendar(identifier: .gregorian)
currentCalendar.timeZone = TimeZone(identifier: item.businessHours.timezoneId) ?? TimeZone.current
timezoneOffsetMinutes = (self.currentTimezone.secondsFromGMT() - currentCalendar.timeZone.secondsFromGMT()) / 60
}
let businessDays: [TelegramBusinessHours.WeekDay] = self.cachedDays
for i in 0 ..< businessDays.count {
let dayTitleValue: String
switch i {
case 0:
dayTitleValue = presentationData.strings.Weekday_Monday
case 1:
dayTitleValue = presentationData.strings.Weekday_Tuesday
case 2:
dayTitleValue = presentationData.strings.Weekday_Wednesday
case 3:
dayTitleValue = presentationData.strings.Weekday_Thursday
case 4:
dayTitleValue = presentationData.strings.Weekday_Friday
case 5:
dayTitleValue = presentationData.strings.Weekday_Saturday
case 6:
dayTitleValue = presentationData.strings.Weekday_Sunday
default:
dayTitleValue = " "
}
let businessHoursText = dayBusinessHoursText(presentationData: presentationData, day: businessDays[i], offsetMinutes: timezoneOffsetMinutes, formatAsPlainText: true)
if !text.isEmpty {
text.append("\n")
}
text.append("\(dayTitleValue): \(businessHoursText)")
}
item.longTapAction(self, text)
item.longTapAction?(self, businessHoursTextToCopy(businessHours: item.businessHours, presentationData: presentationData, displayLocalTimezone: self.displayLocalTimezone))
}
default:
break
@ -271,6 +296,8 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode
self.item = item
self.presentationData = presentationData
self.theme = presentationData.theme
self.containerNode.isGestureEnabled = item.contextAction != nil
let sideInset: CGFloat = 16.0 + safeInsets.left
@ -618,7 +645,7 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode
let hasBottomCorners = hasCorners && bottomItem == nil
self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
self.maskNode.frame = CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height))
transition.updateFrame(node: self.maskNode, frame: CGRect(origin: CGPoint(x: safeInsets.left, y: 0.0), size: CGSize(width: width - safeInsets.left - safeInsets.right, height: height)))
self.bottomSeparatorNode.isHidden = hasBottomCorners
self.activateArea.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: height))

View File

@ -593,6 +593,10 @@ private final class PeerInfoInteraction {
let openBirthdatePrivacy: () -> Void
let openPremiumGift: () -> Void
let editingOpenPersonalChannel: () -> Void
let openUsernameContextMenu: (ASDisplayNode, ContextGesture?) -> Void
let openBioContextMenu: (ASDisplayNode, ContextGesture?) -> Void
let openWorkingHoursContextMenu: (ASDisplayNode, ContextGesture?) -> Void
let openBusinessLocationContextMenu: (ASDisplayNode, ContextGesture?) -> Void
let getController: () -> ViewController?
init(
@ -655,6 +659,10 @@ private final class PeerInfoInteraction {
openBirthdatePrivacy: @escaping () -> Void,
openPremiumGift: @escaping () -> Void,
editingOpenPersonalChannel: @escaping () -> Void,
openUsernameContextMenu: @escaping (ASDisplayNode, ContextGesture?) -> Void,
openBioContextMenu: @escaping (ASDisplayNode, ContextGesture?) -> Void,
openWorkingHoursContextMenu: @escaping (ASDisplayNode, ContextGesture?) -> Void,
openBusinessLocationContextMenu: @escaping (ASDisplayNode, ContextGesture?) -> Void,
getController: @escaping () -> ViewController?
) {
self.openUsername = openUsername
@ -716,6 +724,10 @@ private final class PeerInfoInteraction {
self.openBirthdatePrivacy = openBirthdatePrivacy
self.openPremiumGift = openPremiumGift
self.editingOpenPersonalChannel = editingOpenPersonalChannel
self.openUsernameContextMenu = openUsernameContextMenu
self.openBioContextMenu = openBioContextMenu
self.openWorkingHoursContextMenu = openWorkingHoursContextMenu
self.openBusinessLocationContextMenu = openBusinessLocationContextMenu
self.getController = getController
}
}
@ -1018,7 +1030,7 @@ private func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoStat
}
let ItemNameHelp = 0
let ItemBio = 1
let ItemBio: AnyHashable = AnyHashable("bio_edit")
let ItemBioHelp = 2
let ItemPhoneNumber = 3
let ItemUsername = 4
@ -1180,12 +1192,18 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
items[section] = []
}
let bioContextAction: (ASDisplayNode) -> Void = { sourceNode in
interaction.openPeerInfoContextMenu(.bio, sourceNode, nil)
let bioContextAction: (ASDisplayNode, ContextGesture?, CGPoint?) -> Void = { node, gesture, _ in
interaction.openBioContextMenu(node, gesture)
}
let bioLinkAction: (TextLinkItemActionType, TextLinkItem, ASDisplayNode, CGRect?, Promise<Bool>?) -> Void = { action, item, _, _, _ in
interaction.performBioLinkAction(action, item)
}
let workingHoursContextAction: (ASDisplayNode, ContextGesture?, CGPoint?) -> Void = { node, gesture, _ in
interaction.openWorkingHoursContextMenu(node, gesture)
}
let businessLocationContextAction: (ASDisplayNode, ContextGesture?, CGPoint?) -> Void = { node, gesture, _ in
interaction.openBusinessLocationContextMenu(node, gesture)
}
if let user = data.peer as? TelegramUser {
if !callMessages.isEmpty {
@ -1245,8 +1263,6 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
icon: .qrCode,
action: { _, progress in
interaction.openUsername(mainUsername, true, progress)
}, longTapAction: { sourceNode in
interaction.openPeerInfoContextMenu(.link(customLink: nil), sourceNode, nil)
}, linkItemAction: { type, item, _, _, progress in
if case .tap = type {
if case let .mention(username) = item {
@ -1255,6 +1271,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
}
}, iconAction: {
interaction.openQrCode()
}, contextAction: { node, gesture, _ in
interaction.openUsernameContextMenu(node, gesture)
}, requestLayout: {
interaction.requestLayout(false)
}
@ -1288,7 +1306,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
interaction.requestLayout(false)
}))
} else if let about = cachedData.about, !about.isEmpty {
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.isPremium ? enabledPublicBioEntities : enabledPrivateBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: {
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.isPremium ? enabledPublicBioEntities : enabledPrivateBioEntities), action: nil, linkItemAction: bioLinkAction, contextAction: bioContextAction, requestLayout: {
interaction.requestLayout(false)
}))
}
@ -1296,11 +1314,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
if let businessHours = cachedData.businessHours {
items[.peerInfo]!.append(PeerInfoScreenBusinessHoursItem(id: 300, label: presentationData.strings.PeerInfo_BusinessHours_Label, businessHours: businessHours, requestLayout: { animated in
interaction.requestLayout(animated)
}, longTapAction: { sourceNode, text in
if !text.isEmpty {
interaction.openPeerInfoContextMenu(.businessHours(text), sourceNode, nil)
}
}))
}, longTapAction: nil, contextAction: workingHoursContextAction))
}
if let businessLocation = cachedData.businessLocation {
@ -1314,11 +1328,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
action: {
interaction.openLocation()
},
longTapAction: { sourceNode, text in
if !text.isEmpty {
interaction.openPeerInfoContextMenu(.businessLocation(text), sourceNode, nil)
}
}
contextAction: businessLocationContextAction
))
} else {
items[.peerInfo]!.append(PeerInfoScreenAddressItem(
@ -1327,11 +1337,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
text: businessLocation.address,
imageSignal: nil,
action: nil,
longTapAction: { sourceNode, text in
if !text.isEmpty {
interaction.openPeerInfoContextMenu(.businessLocation(text), sourceNode, nil)
}
}
contextAction: businessLocationContextAction
))
}
}
@ -1534,7 +1540,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
if case .group = channel.info {
enabledEntities = enabledPrivateBioEntities
}
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Channel_Info_Description, text: aboutText, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: {
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: ItemAbout, label: presentationData.strings.Channel_Info_Description, text: aboutText, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledEntities), action: nil, linkItemAction: bioLinkAction, contextAction: bioContextAction, requestLayout: {
interaction.requestLayout(true)
}))
}
@ -1584,7 +1590,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
}
if let aboutText = aboutText {
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.Channel_Info_Description, text: aboutText, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledPrivateBioEntities), action: nil, longTapAction: bioContextAction, linkItemAction: bioLinkAction, requestLayout: {
items[.peerInfo]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: presentationData.strings.Channel_Info_Description, text: aboutText, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: enabledPrivateBioEntities), action: nil, linkItemAction: bioLinkAction, contextAction: bioContextAction, requestLayout: {
interaction.requestLayout(true)
}))
}
@ -2748,6 +2754,26 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
return
}
self.editingOpenPersonalChannel()
}, openUsernameContextMenu: { [weak self] node, gesture in
guard let self else {
return
}
self.openUsernameContextMenu(node: node, gesture: gesture)
}, openBioContextMenu: { [weak self] node, gesture in
guard let self else {
return
}
self.openBioContextMenu(node: node, gesture: gesture)
}, openWorkingHoursContextMenu: { [weak self] node, gesture in
guard let self else {
return
}
self.openWorkingHoursContextMenu(node: node, gesture: gesture)
}, openBusinessLocationContextMenu: { [weak self] node, gesture in
guard let self else {
return
}
self.openBusinessLocationContextMenu(node: node, gesture: gesture)
},
getController: { [weak self] in
return self?.controller
@ -4585,6 +4611,12 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
var previousIsBlocked: Bool?
var currentIsBlocked: Bool?
var previousBusinessHours: TelegramBusinessHours?
var currentBusinessHours: TelegramBusinessHours?
var previousBusinessLocation: TelegramBusinessLocation?
var currentBusinessLocation: TelegramBusinessLocation?
var previousPhotoIsPersonal: Bool?
var currentPhotoIsPersonal: Bool?
if let previousUser = previousData?.peer as? TelegramUser {
@ -4613,6 +4645,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
currentAbout = cachedData.about
previousIsBlocked = previousCachedData.isBlocked
currentIsBlocked = cachedData.isBlocked
previousBusinessHours = previousCachedData.businessHours
currentBusinessHours = cachedData.businessHours
previousBusinessLocation = previousCachedData.businessLocation
currentBusinessLocation = cachedData.businessLocation
}
if self.isSettings {
@ -4641,6 +4677,16 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
if let previousIsBlocked, let currentIsBlocked, previousIsBlocked != currentIsBlocked {
infoUpdated = true
}
if previousData != nil {
if (previousBusinessHours == nil) != (currentBusinessHours != nil) {
infoUpdated = true
}
if (previousBusinessLocation == nil) != (currentBusinessLocation != nil) {
infoUpdated = true
}
}
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: self.didSetReady && (membersUpdated || infoUpdated) ? .animated(duration: 0.3, curve: .spring) : .immediate)
if let cachedData = data.cachedData as? CachedUserData, let _ = cachedData.birthday {
@ -6846,6 +6892,268 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
}
private func openUsernameContextMenu(node: ASDisplayNode, gesture: ContextGesture?) {
guard let sourceNode = node as? ContextExtractedContentContainingNode else {
return
}
guard let peer = self.data?.peer else {
return
}
guard let username = peer.addressName else {
return
}
let copyAction = { [weak self] in
guard let self else {
return
}
UIPasteboard.general.string = "@\(username)"
//TODO:localize
self.controller?.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: "Username copied"), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
}
var items: [ContextMenuItem] = []
if self.isMyProfile {
items.append(.action(ContextMenuActionItem(text: "Edit Username", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in
c.dismiss {
guard let self else {
return
}
self.openSettings(section: .username)
}
})))
}
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Copy Username", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
c.dismiss {
copyAction()
}
})))
let actions = ContextController.Items(content: .list(items))
let contextController = ContextController(presentationData: self.presentationData, source: .extracted(PeerInfoContextExtractedContentSource(sourceNode: sourceNode)), items: .single(actions), gesture: gesture)
self.controller?.present(contextController, in: .window(.root))
}
private func openBioContextMenu(node: ASDisplayNode, gesture: ContextGesture?) {
guard let sourceNode = node as? ContextExtractedContentContainingNode else {
return
}
guard let cachedData = self.data?.cachedData else {
return
}
var bioText: String?
if let cachedData = cachedData as? CachedUserData {
bioText = cachedData.about
} else if let cachedData = cachedData as? CachedChannelData {
bioText = cachedData.about
} else if let cachedData = cachedData as? CachedGroupData {
bioText = cachedData.about
}
guard let bioText, !bioText.isEmpty else {
return
}
let copyAction = { [weak self] in
guard let self else {
return
}
UIPasteboard.general.string = bioText
//TODO:localize
self.controller?.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: "Bio copied"), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
}
var items: [ContextMenuItem] = []
if self.isMyProfile {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Edit Bio", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in
c.dismiss {
guard let self else {
return
}
self.headerNode.navigationButtonContainer.performAction?(.edit, nil, nil)
for (_, section) in self.editingSections {
for (id, itemNode) in section.itemNodes {
if id == AnyHashable("bio_edit") {
if let itemNode = itemNode as? PeerInfoScreenMultilineInputItemNode {
itemNode.focus()
}
break
}
}
}
}
})))
}
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Copy Bio", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
c.dismiss {
copyAction()
}
})))
let actions = ContextController.Items(content: .list(items))
let contextController = ContextController(presentationData: self.presentationData, source: .extracted(PeerInfoContextExtractedContentSource(sourceNode: sourceNode)), items: .single(actions), gesture: gesture)
self.controller?.present(contextController, in: .window(.root))
}
private func openWorkingHoursContextMenu(node: ASDisplayNode, gesture: ContextGesture?) {
guard let sourceNode = node as? ContextExtractedContentContainingNode else {
return
}
guard let cachedData = self.data?.cachedData else {
return
}
var businessHours: TelegramBusinessHours?
if let cachedData = cachedData as? CachedUserData {
businessHours = cachedData.businessHours
}
guard let businessHours else {
return
}
let copyAction = { [weak self] in
guard let self else {
return
}
UIPasteboard.general.string = businessHoursTextToCopy(businessHours: businessHours, presentationData: self.presentationData, displayLocalTimezone: false)
//TODO:localize
self.controller?.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: "Working hours copied."), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
}
var items: [ContextMenuItem] = []
if self.isMyProfile {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Edit Hours", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in
c.dismiss {
guard let self else {
return
}
let businessHoursSetupScreen = self.context.sharedContext.makeBusinessHoursSetupScreen(context: self.context, initialValue: businessHours, completion: { _ in })
self.controller?.push(businessHoursSetupScreen)
}
})))
}
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Copy Hours", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
c.dismiss {
copyAction()
}
})))
if self.isMyProfile {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Remove", textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] c, _ in
c.dismiss {
guard let self else {
return
}
let _ = self.context.engine.accountData.updateAccountBusinessHours(businessHours: nil).startStandalone()
}
})))
}
let actions = ContextController.Items(content: .list(items))
let contextController = ContextController(presentationData: self.presentationData, source: .extracted(PeerInfoContextExtractedContentSource(sourceNode: sourceNode)), items: .single(actions), gesture: gesture)
self.controller?.present(contextController, in: .window(.root))
}
private func openBusinessLocationContextMenu(node: ASDisplayNode, gesture: ContextGesture?) {
guard let sourceNode = node as? ContextExtractedContentContainingNode else {
return
}
guard let cachedData = self.data?.cachedData else {
return
}
var businessLocation: TelegramBusinessLocation?
if let cachedData = cachedData as? CachedUserData {
businessLocation = cachedData.businessLocation
}
guard let businessLocation else {
return
}
let copyAction = { [weak self] in
guard let self else {
return
}
UIPasteboard.general.string = businessLocation.address
//TODO:localize
self.controller?.present(UndoOverlayController(presentationData: self.presentationData, content: .copy(text: "Working hours copied."), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .current)
}
var items: [ContextMenuItem] = []
if businessLocation.coordinates != nil {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Open in Maps", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Media Editor/LocationSmall"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in
c.dismiss(completion: {
guard let self else {
return
}
self.interaction.openLocation()
})
})))
}
if !businessLocation.address.isEmpty {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Copy Address", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
c.dismiss {
copyAction()
}
})))
}
if self.isMyProfile {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Edit Location", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in
c.dismiss {
guard let self else {
return
}
let businessLocationSetupScreen = self.context.sharedContext.makeBusinessLocationSetupScreen(context: self.context, initialValue: businessLocation, completion: { _ in })
self.controller?.push(businessLocationSetupScreen)
}
})))
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Remove", textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] c, _ in
c.dismiss {
guard let self else {
return
}
let _ = self.context.engine.accountData.updateAccountBusinessLocation(businessLocation: nil).startStandalone()
}
})))
}
let actions = ContextController.Items(content: .list(items))
let contextController = ContextController(presentationData: self.presentationData, source: .extracted(PeerInfoContextExtractedContentSource(sourceNode: sourceNode)), items: .single(actions), gesture: gesture)
self.controller?.present(contextController, in: .window(.root))
}
private func openPhone(value: String, node: ASDisplayNode, gesture: ContextGesture?, progress: Promise<Bool>?) {
guard let sourceNode = node as? ContextExtractedContentContainingNode else {
return
@ -6919,45 +7227,66 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
var isAnonymousNumber = false
var items: [ContextMenuItem] = []
if strongSelf.isMyProfile {
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Change Number", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in
c.dismiss {
guard let self else {
return
}
self.openSettings(section: .phoneNumber)
}
})))
}
if case let .user(peer) = peer, let peerPhoneNumber = peer.phone, formattedPhoneNumber == formatPhoneNumber(context: strongSelf.context, number: peerPhoneNumber) {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.UserInfo_TelegramCall, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Call"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
c.dismiss {
telegramCallAction(false)
}
})))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.UserInfo_TelegramVideoCall, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/VideoCall"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
c.dismiss {
telegramCallAction(true)
}
})))
if !formattedPhoneNumber.hasPrefix("+888") {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.UserInfo_PhoneCall, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/PhoneCall"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
if !strongSelf.isMyProfile {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.UserInfo_TelegramCall, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Call"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
c.dismiss {
phoneCallAction()
telegramCallAction(false)
}
})))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.UserInfo_TelegramVideoCall, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/VideoCall"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
c.dismiss {
telegramCallAction(true)
}
})))
}
if !formattedPhoneNumber.hasPrefix("+888") {
if !strongSelf.isMyProfile {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.UserInfo_PhoneCall, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/PhoneCall"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
c.dismiss {
phoneCallAction()
}
})))
}
} else {
isAnonymousNumber = true
}
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ContextMenuCopy, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Copy Number", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
c.dismiss {
copyAction()
}
})))
} else {
if !formattedPhoneNumber.hasPrefix("+888") {
items.append(
.action(ContextMenuActionItem(text: presentationData.strings.UserInfo_PhoneCall, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/PhoneCall"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
c.dismiss {
phoneCallAction()
}
}))
)
if !strongSelf.isMyProfile {
items.append(
.action(ContextMenuActionItem(text: presentationData.strings.UserInfo_PhoneCall, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/PhoneCall"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
c.dismiss {
phoneCallAction()
}
}))
)
}
} else {
isAnonymousNumber = true
}
//TODO:localize
items.append(
.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ContextMenuCopy, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
.action(ContextMenuActionItem(text: "Copy Number", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
c.dismiss {
copyAction()
}
@ -9196,21 +9525,26 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
guard let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController else {
return
}
var updatedControllers = navigationController.viewControllers
for controller in navigationController.viewControllers.reversed() {
if controller !== strongSelf && !(controller is TabBarController) {
updatedControllers.removeLast()
} else {
break
}
}
updatedControllers.append(c)
var animated = true
if let validLayout = strongSelf.validLayout?.0, case .regular = validLayout.metrics.widthClass {
animated = false
if strongSelf.isMyProfile {
navigationController.pushViewController(c)
} else {
var updatedControllers = navigationController.viewControllers
for controller in navigationController.viewControllers.reversed() {
if controller !== strongSelf && !(controller is TabBarController) {
updatedControllers.removeLast()
} else {
break
}
}
updatedControllers.append(c)
var animated = true
if let validLayout = strongSelf.validLayout?.0, case .regular = validLayout.metrics.widthClass {
animated = false
}
navigationController.setViewControllers(updatedControllers, animated: animated)
}
navigationController.setViewControllers(updatedControllers, animated: animated)
}
switch section {
case .avatar:

View File

@ -36,7 +36,7 @@ final class PeerInfoScreenMultilineInputItemNode: PeerInfoScreenItemNode {
private let bottomSeparatorNode: ASDisplayNode
private let maskNode: ASImageNode
private var item: PeerInfoScreenMultilineInputItem?
private(set) var item: PeerInfoScreenMultilineInputItem?
private var itemNode: ItemListMultilineInputItemNode?
override init() {
@ -127,4 +127,8 @@ final class PeerInfoScreenMultilineInputItemNode: PeerInfoScreenItemNode {
func animateErrorIfNeeded() {
self.itemNode?.animateErrorIfNeeded()
}
func focus() {
self.itemNode?.focus()
}
}

View File

@ -2088,7 +2088,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
if apply {
switch strongSelf.chatLocation {
case .peer, .replyThread:
if !strongSelf.context.sharedContext.immediateExperimentalUISettings.skipReadHistory {
if !strongSelf.context.sharedContext.immediateExperimentalUISettings.skipReadHistory && !strongSelf.context.account.isSupportUser {
strongSelf.context.applyMaxReadIndex(for: strongSelf.chatLocation, contextHolder: strongSelf.chatLocationContextHolder, messageIndex: messageIndex)
}
case .customChatContents:

View File

@ -78,6 +78,7 @@ private struct AccountAttributes: Equatable {
let sortIndex: Int32
let isTestingEnvironment: Bool
let backupData: AccountBackupData?
let isSupportUser: Bool
}
private enum AddedAccountResult {
@ -516,14 +517,17 @@ public final class SharedAccountContextImpl: SharedAccountContext {
})
var backupData: AccountBackupData?
var sortIndex: Int32 = 0
var isSupportUser = false
for attribute in record.attributes {
if case let .sortOrder(sortOrder) = attribute {
sortIndex = sortOrder.order
} else if case let .backupData(backupDataValue) = attribute {
backupData = backupDataValue.data
} else if case .supportUserInfo = attribute {
isSupportUser = true
}
}
result[record.id] = AccountAttributes(sortIndex: sortIndex, isTestingEnvironment: isTestingEnvironment, backupData: backupData)
result[record.id] = AccountAttributes(sortIndex: sortIndex, isTestingEnvironment: isTestingEnvironment, backupData: backupData, isSupportUser: isSupportUser)
}
let authRecord: (AccountRecordId, Bool)? = view.currentAuthAccount.flatMap({ authAccount in
let isTestingEnvironment = authAccount.attributes.contains(where: { attribute in
@ -557,7 +561,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
var addedAuthSignal: Signal<UnauthorizedAccount?, NoError> = .single(nil)
for (id, attributes) in records {
if self.activeAccountsValue?.accounts.firstIndex(where: { $0.0 == id}) == nil {
addedSignals.append(accountWithId(accountManager: accountManager, networkArguments: networkArguments, id: id, encryptionParameters: encryptionParameters, supplementary: !applicationBindings.isMainApp, rootPath: rootPath, beginWithTestingEnvironment: attributes.isTestingEnvironment, backupData: attributes.backupData, auxiliaryMethods: makeTelegramAccountAuxiliaryMethods(uploadInBackground: appDelegate?.uploadInBackround))
addedSignals.append(accountWithId(accountManager: accountManager, networkArguments: networkArguments, id: id, encryptionParameters: encryptionParameters, supplementary: !applicationBindings.isMainApp, isSupportUser: attributes.isSupportUser, rootPath: rootPath, beginWithTestingEnvironment: attributes.isTestingEnvironment, backupData: attributes.backupData, auxiliaryMethods: makeTelegramAccountAuxiliaryMethods(uploadInBackground: appDelegate?.uploadInBackround))
|> mapToSignal { result -> Signal<AddedAccountResult, NoError> in
switch result {
case let .authorized(account):
@ -581,7 +585,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}
}
if let authRecord = authRecord, authRecord.0 != self.activeAccountsValue?.currentAuth?.id {
addedAuthSignal = accountWithId(accountManager: accountManager, networkArguments: networkArguments, id: authRecord.0, encryptionParameters: encryptionParameters, supplementary: !applicationBindings.isMainApp, rootPath: rootPath, beginWithTestingEnvironment: authRecord.1, backupData: nil, auxiliaryMethods: makeTelegramAccountAuxiliaryMethods(uploadInBackground: appDelegate?.uploadInBackround))
addedAuthSignal = accountWithId(accountManager: accountManager, networkArguments: networkArguments, id: authRecord.0, encryptionParameters: encryptionParameters, supplementary: !applicationBindings.isMainApp, isSupportUser: false, rootPath: rootPath, beginWithTestingEnvironment: authRecord.1, backupData: nil, auxiliaryMethods: makeTelegramAccountAuxiliaryMethods(uploadInBackground: appDelegate?.uploadInBackround))
|> mapToSignal { result -> Signal<UnauthorizedAccount?, NoError> in
switch result {
case let .unauthorized(account):