Various improvements

This commit is contained in:
Ilya Laktyushin 2024-12-06 09:31:49 +04:00
parent c7ab78a8a1
commit fdb4b80e27
56 changed files with 520 additions and 153 deletions

View File

@ -155,6 +155,7 @@ public struct ChatAvailableMessageActionOptions: OptionSet {
public static let sendScheduledNow = ChatAvailableMessageActionOptions(rawValue: 1 << 8)
public static let editScheduledTime = ChatAvailableMessageActionOptions(rawValue: 1 << 9)
public static let externalShare = ChatAvailableMessageActionOptions(rawValue: 1 << 10)
public static let sendGift = ChatAvailableMessageActionOptions(rawValue: 1 << 11)
}
public struct ChatAvailableMessageActions {
@ -802,6 +803,7 @@ public protocol TelegramRootControllerInterface: NavigationController {
func getPrivacySettings() -> Promise<AccountPrivacySettings?>?
func openSettings()
func openBirthdaySetup()
func openPhotoSetup()
}
public protocol QuickReplySetupScreenInitialData: AnyObject {

View File

@ -959,6 +959,7 @@ public protocol PeerInfoScreen: ViewController {
func toggleStorySelection(ids: [Int32], isSelected: Bool)
func togglePaneIsReordering(isReordering: Bool)
func cancelItemSelection()
func openAvatarSetup()
}
public extension Peer {

View File

@ -24,6 +24,7 @@ private let repliesIcon = generateTintedImage(image: UIImage(bundleImageName: "A
private let anonymousSavedMessagesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/AnonymousSenderIcon"), color: .white)
private let anonymousSavedMessagesDarkIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/AnonymousSenderIcon"), color: UIColor(white: 1.0, alpha: 0.4))
private let myNotesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/MyNotesIcon"), color: .white)
private let cameraIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/CameraIcon"), color: .white)
public func avatarPlaceholderFont(size: CGFloat) -> UIFont {
return Font.with(size: size, design: .round, weight: .bold)
@ -86,7 +87,7 @@ public func calculateAvatarColors(context: AccountContext?, explicitColorIndex:
}
let colors: [UIColor]
if icon != .none {
if icon != .none && icon != .cameraIcon {
if case .deletedIcon = icon {
colors = AvatarNode.grayscaleColors
} else if case .phoneIcon = icon {
@ -196,6 +197,7 @@ public enum AvatarNodeIcon: Equatable {
case deletedIcon
case phoneIcon
case repostIcon
case cameraIcon
}
public enum AvatarNodeImageOverride: Equatable {
@ -210,6 +212,7 @@ public enum AvatarNodeImageOverride: Equatable {
case deletedIcon
case phoneIcon
case repostIcon
case cameraIcon
}
public enum AvatarNodeColorOverride {
@ -540,6 +543,9 @@ public final class AvatarNode: ASDisplayNode {
case .phoneIcon:
representation = nil
icon = .phoneIcon
case .cameraIcon:
representation = nil
icon = .cameraIcon
}
} else if peer?.restrictionText(platform: "ios", contentSettings: contentSettings) == nil {
representation = peer?.smallProfileImage
@ -716,6 +722,9 @@ public final class AvatarNode: ASDisplayNode {
case .phoneIcon:
representation = nil
icon = .phoneIcon
case .cameraIcon:
representation = nil
icon = .cameraIcon
}
} else if peer?.restrictionText(platform: "ios", contentSettings: genericContext.currentContentSettings.with { $0 }) == nil {
representation = peer?.smallProfileImage
@ -959,6 +968,15 @@ public final class AvatarNode: ASDisplayNode {
if let myNotesIcon = myNotesIcon {
context.draw(myNotesIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - myNotesIcon.size.width) / 2.0), y: floor((bounds.size.height - myNotesIcon.size.height) / 2.0)), size: myNotesIcon.size))
}
} else if case .cameraIcon = parameters.icon {
let factor = bounds.size.width / 40.0
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: factor, y: -factor)
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
if let cameraIcon = cameraIcon {
context.draw(cameraIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - cameraIcon.size.width) / 2.0), y: floor((bounds.size.height - cameraIcon.size.height) / 2.0)), size: cameraIcon.size))
}
} else if case .editAvatarIcon = parameters.icon, let theme = parameters.theme, !parameters.hasImage {
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)

View File

@ -246,7 +246,9 @@ public func peerAvatarImage(postbox: Postbox, network: Network, peerReference: P
}
}
context.draw(dataImage, in: CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset))
let filledSize = CGSize(width: dataImage.width, height: dataImage.height).aspectFilled(displayDimensions)
context.draw(dataImage, in: CGRect(origin: CGPoint(x: floor((displayDimensions.width - filledSize.width) / 2.0), y: floor((displayDimensions.height - filledSize.height) / 2.0)), size: filledSize).insetBy(dx: inset, dy: inset))
if blurred {
context.setBlendMode(.normal)
context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.45).cgColor)

View File

@ -162,6 +162,7 @@ public final class BrowserBookmarksScreen: ViewController {
}, openAgeRestrictedMessageMedia: { _, _ in
}, playMessageEffect: { _ in
}, editMessageFactCheck: { _ in
}, sendGift: { _ in
}, requestMessageUpdate: { _, _ in
}, cancelInteractiveKeyboardGestures: {
}, dismissTextInput: {

View File

@ -214,7 +214,8 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode {
theme: PresentationTheme,
mode: HorizontalPeerItemMode,
strings: PresentationStrings,
peerSelected: @escaping (EnginePeer) -> Void, peerContextAction: @escaping (EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void, isPeerSelected: @escaping (EnginePeer.Id) -> Bool, share: Bool = false) {
peerSelected: @escaping (EnginePeer) -> Void, peerContextAction: @escaping (EnginePeer, ASDisplayNode, ContextGesture?, CGPoint?) -> Void, isPeerSelected: @escaping (EnginePeer.Id) -> Bool, share: Bool = false)
{
self.theme = theme
self.strings = strings
self.themeAndStringsPromise = Promise((self.theme, self.strings))
@ -225,6 +226,7 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode {
self.isPeerSelected = isPeerSelected
self.listView = ListView()
self.listView.preloadPages = false
self.listView.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
self.listView.accessibilityPageScrolledString = { row, count in
return strings.VoiceOver_ScrollStatus(row, count).string
@ -340,11 +342,6 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode {
)
strongSelf.enqueueTransition(transition)
if !strongSelf.didSetReady {
strongSelf.ready.set(.single(true))
strongSelf.didSetReady = true
}
}
}))
if case .actionSheet = mode {
@ -371,7 +368,20 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode {
} else if transition.animated {
options.insert(.AnimateInsertion)
}
self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { _ in })
self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { [weak self] _ in
guard let self else {
return
}
if !self.didSetReady {
self.ready.set(.single(true))
self.didSetReady = true
}
if !self.listView.preloadPages {
Queue.mainQueue().after(0.5) {
self.listView.preloadPages = true
}
}
})
}
}

View File

@ -1202,6 +1202,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
)
}
self.chatListDisplayNode.mainContainerNode.openPhotoSetup = { [weak self] in
guard let self else {
return
}
if let rootController = self.navigationController as? TelegramRootControllerInterface {
rootController.openPhotoSetup()
}
}
self.chatListDisplayNode.mainContainerNode.openPremiumManagement = { [weak self] in
guard let self else {
return

View File

@ -354,6 +354,9 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
itemNode.listNode.openWebApp = { [weak self] amount in
self?.openWebApp?(amount)
}
itemNode.listNode.openPhotoSetup = { [weak self] in
self?.openPhotoSetup?()
}
self.currentItemStateValue.set(itemNode.listNode.state |> map { state in
let filterId: Int32?
@ -421,6 +424,7 @@ public final class ChatListContainerNode: ASDisplayNode, ASGestureRecognizerDele
var openStories: ((ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void)?
var openStarsTopup: ((Int64?) -> Void)?
var openWebApp: ((TelegramUser) -> Void)?
var openPhotoSetup: (() -> Void)?
var addedVisibleChatsWithPeerIds: (([EnginePeer.Id]) -> Void)?
var didBeginSelectingChats: (() -> Void)?
var canExpandHiddenItems: (() -> Bool)?

View File

@ -115,7 +115,7 @@ private enum ChatListRecentEntry: Comparable, Identifiable {
presentationData: ChatListPresentationData,
filter: ChatListNodePeersFilter,
key: ChatListSearchPaneKey,
peerSelected: @escaping (EnginePeer, Int64?, Bool) -> Void,
peerSelected: @escaping (EnginePeer, Int64?, Bool, OpenPeerAction) -> Void,
disabledPeerSelected: @escaping (EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void,
peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?,
clearRecentlySearchedPeers: @escaping () -> Void,
@ -130,7 +130,7 @@ private enum ChatListRecentEntry: Comparable, Identifiable {
switch self {
case let .topPeers(peers, theme, strings):
return ChatListRecentPeersListItem(theme: theme, strings: strings, context: context, peers: peers, peerSelected: { peer in
peerSelected(peer, nil, false)
peerSelected(peer, nil, false, .generic)
}, peerContextAction: { peer, node, gesture, location in
if let peerContextAction = peerContextAction {
peerContextAction(peer, .recentPeers, node, gesture, location)
@ -287,21 +287,28 @@ private enum ChatListRecentEntry: Comparable, Identifiable {
}
var buttonAction: ContactsPeerItemButtonAction?
if case .chats = key, case let .user(user) = primaryPeer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp) {
if [.chats, .apps].contains(key), case let .user(user) = primaryPeer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp) {
buttonAction = ContactsPeerItemButtonAction(
title: presentationData.strings.ChatList_Search_Open,
action: { peer, _, _ in
peerSelected(primaryPeer, nil, true)
peerSelected(primaryPeer, nil, false, .openApp)
}
)
}
var peerMode: ContactsPeerItemPeerMode
if case .apps = key {
peerMode = .app(isPopular: section == .popularApps)
} else {
peerMode = .generalSearch(isSavedMessages: false)
}
return ContactsPeerItem(
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder, dateTimeFormat: presentationData.dateTimeFormat),
sortOrder: nameSortOrder,
displayOrder: nameDisplayOrder,
context: context,
peerMode: .generalSearch(isSavedMessages: false),
peerMode: peerMode,
peer: .peer(peer: primaryPeer, chatPeer: chatPeer),
status: status,
badge: badge,
@ -315,7 +322,7 @@ private enum ChatListRecentEntry: Comparable, Identifiable {
alwaysShowLastSeparator: key == .apps,
action: { _ in
if let chatPeer = peer.peer.peers[peer.peer.peerId] {
peerSelected(EnginePeer(chatPeer), nil, section == .recommendedChannels || section == .popularApps)
peerSelected(EnginePeer(chatPeer), nil, section == .recommendedChannels, section == .popularApps ? .info : .generic)
}
},
disabledAction: { _ in
@ -1067,6 +1074,12 @@ public struct ChatListSearchContainerTransition {
}
}
enum OpenPeerAction {
case generic
case info
case openApp
}
private func chatListSearchContainerPreparedRecentTransition(
from fromEntries: [ChatListRecentEntry],
to toEntries: [ChatListRecentEntry],
@ -1075,7 +1088,7 @@ private func chatListSearchContainerPreparedRecentTransition(
presentationData: ChatListPresentationData,
filter: ChatListNodePeersFilter,
key: ChatListSearchPaneKey,
peerSelected: @escaping (EnginePeer, Int64?, Bool) -> Void,
peerSelected: @escaping (EnginePeer, Int64?, Bool, OpenPeerAction) -> Void,
disabledPeerSelected: @escaping (EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void,
peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?,
clearRecentlySearchedPeers: @escaping () -> Void,
@ -1468,6 +1481,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
self.selectedMessagesPromise.set(.single(self.selectedMessages))
self.recentListNode = ListView()
self.recentListNode.preloadPages = false
self.recentListNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor
self.recentListNode.accessibilityPageScrolledString = { row, count in
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
@ -3014,6 +3028,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}, dismissNotice: { _ in
}, editPeer: { _ in
}, openWebApp: { _ in
}, openPhotoSetup: {
})
chatListInteraction.isSearchMode = true
@ -3822,7 +3837,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}
}
let transition = chatListSearchContainerPreparedRecentTransition(from: previousRecentItems?.entries ?? [], to: recentItems.entries, forceUpdateAll: forceUpdateAll, context: context, presentationData: presentationData, filter: peersFilter, key: key, peerSelected: { peer, threadId, isRecommended in
let transition = chatListSearchContainerPreparedRecentTransition(from: previousRecentItems?.entries ?? [], to: recentItems.entries, forceUpdateAll: forceUpdateAll, context: context, presentationData: presentationData, filter: peersFilter, key: key, peerSelected: { peer, threadId, isRecommended, action in
guard let self else {
return
}
@ -3848,36 +3863,39 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}
} else if case .apps = key {
if let navigationController = self.navigationController {
if isRecommended {
if let peerInfoScreen = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
navigationController.pushViewController(peerInfoScreen)
}
} else if case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), let parentController = self.parentController {
self.context.sharedContext.openWebApp(
context: self.context,
parentController: parentController,
updatedPresentationData: nil,
botPeer: peer,
chatPeer: nil,
threadId: nil,
buttonText: "",
url: "",
simple: true,
source: .generic,
skipTermsOfService: true,
payload: nil
)
} else {
switch action {
case .generic:
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(
navigationController: navigationController,
context: self.context,
chatLocation: .peer(peer),
keepStack: .always
))
case .info:
if let peerInfoScreen = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
navigationController.pushViewController(peerInfoScreen)
}
case .openApp:
if let parentController = self.parentController {
self.context.sharedContext.openWebApp(
context: self.context,
parentController: parentController,
updatedPresentationData: nil,
botPeer: peer,
chatPeer: nil,
threadId: nil,
buttonText: "",
url: "",
simple: true,
source: .generic,
skipTermsOfService: true,
payload: nil
)
}
}
}
} else {
if isRecommended, case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), let parentController = self.parentController {
if case .openApp = action, case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), let parentController = self.parentController {
self.context.sharedContext.openWebApp(
context: self.context,
parentController: parentController,
@ -4581,6 +4599,11 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
strongSelf.ready.set(.single(true))
}
strongSelf.didSetReady = true
if !strongSelf.recentListNode.preloadPages {
Queue.mainQueue().after(0.5) {
strongSelf.recentListNode.preloadPages = true
}
}
strongSelf.emptyRecentAnimationNode?.isHidden = !transition.isEmpty
strongSelf.emptyRecentTitleNode?.isHidden = !transition.isEmpty
@ -4907,6 +4930,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode {
}, dismissNotice: { _ in
}, editPeer: { _ in
}, openWebApp: { _ in
}, openPhotoSetup: {
})
var isInlineMode = false
if case .topics = key {

View File

@ -160,6 +160,7 @@ public final class ChatListShimmerNode: ASDisplayNode {
}, dismissNotice: { _ in
}, editPeer: { _ in
}, openWebApp: { _ in
}, openPhotoSetup: {
})
interaction.isInlineMode = isInlineMode

View File

@ -113,6 +113,7 @@ public final class ChatListNodeInteraction {
let dismissNotice: (ChatListNotice) -> Void
let editPeer: (ChatListItem) -> Void
let openWebApp: (TelegramUser) -> Void
let openPhotoSetup: () -> Void
public var searchTextHighightState: String?
var highlightedChatLocation: ChatListHighlightedLocation?
@ -169,7 +170,8 @@ public final class ChatListNodeInteraction {
openStarsTopup: @escaping (Int64?) -> Void,
dismissNotice: @escaping (ChatListNotice) -> Void,
editPeer: @escaping (ChatListItem) -> Void,
openWebApp: @escaping (TelegramUser) -> Void
openWebApp: @escaping (TelegramUser) -> Void,
openPhotoSetup: @escaping () -> Void
) {
self.activateSearch = activateSearch
self.peerSelected = peerSelected
@ -214,6 +216,7 @@ public final class ChatListNodeInteraction {
self.dismissNotice = dismissNotice
self.editPeer = editPeer
self.openWebApp = openWebApp
self.openPhotoSetup = openPhotoSetup
}
}
@ -519,10 +522,13 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
default:
break
}
}
if canManage {
} else if case let .channel(peer) = peer, case .group = peer.info, peer.hasPermission(.inviteMembers) {
canManage = true
} else if case let .channel(peer) = peer, case .broadcast = peer.info, peer.hasPermission(.addAdmins) {
canManage = true
}
if canManage {
} else {
enabled = false
}
@ -759,6 +765,8 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
break
case let .starsSubscriptionLowBalance(amount, _):
nodeInteraction?.openStarsTopup(amount.value)
case .setupPhoto:
nodeInteraction?.openPhotoSetup()
}
case .hide:
nodeInteraction?.dismissNotice(notice)
@ -1103,6 +1111,8 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
break
case let .starsSubscriptionLowBalance(amount, _):
nodeInteraction?.openStarsTopup(amount.value)
case .setupPhoto:
nodeInteraction?.openPhotoSetup()
}
case .hide:
nodeInteraction?.dismissNotice(notice)
@ -1224,6 +1234,7 @@ public final class ChatListNode: ListView {
public var openPremiumManagement: (() -> Void)?
public var openStarsTopup: ((Int64?) -> Void)?
public var openWebApp: ((TelegramUser) -> Void)?
public var openPhotoSetup: (() -> Void)?
private var theme: PresentationTheme
@ -1876,6 +1887,11 @@ public final class ChatListNode: ListView {
return
}
self.openWebApp?(user)
}, openPhotoSetup: { [weak self] in
guard let self else {
return
}
self.openPhotoSetup?()
})
nodeInteraction.isInlineMode = isInlineMode
@ -1970,11 +1986,16 @@ public final class ChatListNode: ListView {
context.engine.notices.getServerDismissedSuggestions(),
twoStepData,
newSessionReviews(postbox: context.account.postbox),
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Birthday(id: context.account.peerId)),
context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId),
TelegramEngine.EngineData.Item.Peer.Birthday(id: context.account.peerId)
),
context.account.stateManager.contactBirthdays,
starsSubscriptionsContextPromise.get()
)
|> mapToSignal { suggestions, dismissedSuggestions, configuration, newSessionReviews, birthday, birthdays, starsSubscriptionsContext -> Signal<ChatListNotice?, NoError> in
|> mapToSignal { suggestions, dismissedSuggestions, configuration, newSessionReviews, data, birthdays, starsSubscriptionsContext -> Signal<ChatListNotice?, NoError> in
let (accountPeer, birthday) = data
if let newSessionReview = newSessionReviews.first {
return .single(.reviewLogin(newSessionReview: newSessionReview, totalCount: newSessionReviews.count))
}
@ -2025,6 +2046,8 @@ public final class ChatListNode: ListView {
starsSubscriptionsContextPromise.set(.single(context.engine.payments.peerStarsSubscriptionsContext(starsContext: nil, missingBalance: true)))
return .single(nil)
}
} else if suggestions.contains(.setupPhoto), let accountPeer, accountPeer.smallProfileImage == nil {
return .single(.setupPhoto(accountPeer))
} else if suggestions.contains(.gracePremium) {
return .single(.premiumGrace)
} else if suggestions.contains(.setupBirthday) && birthday == nil {
@ -2413,10 +2436,13 @@ public final class ChatListNode: ListView {
default:
break
}
} else if case let .channel(peer) = peer, case .group = peer.info, peer.hasPermission(.inviteMembers) {
canManage = true
} else if case let .channel(peer) = peer, case .broadcast = peer.info, peer.hasPermission(.addAdmins) {
canManage = true
}
if canManage {
} else if case let .channel(peer) = peer, case .group = peer.info, peer.hasPermission(.inviteMembers) {
} else {
return false
}

View File

@ -91,6 +91,7 @@ public enum ChatListNotice: Equatable {
case reviewLogin(newSessionReview: NewSessionReview, totalCount: Int)
case premiumGrace
case starsSubscriptionLowBalance(amount: StarsAmount, peers: [EnginePeer])
case setupPhoto(EnginePeer)
}
enum ChatListNodeEntry: Comparable, Identifiable {
@ -605,6 +606,7 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState,
var result: [ChatListNodeEntry] = []
var hasContacts = false
if !view.hasEarlier {
var existingPeerIds = Set<EnginePeer.Id>()
for item in view.items {
@ -620,8 +622,9 @@ func chatListNodeEntriesForView(view: EngineChatList, state: ChatListNodeState,
peer: contact.peer,
presence: contact.presence
)))
hasContacts = true
}
if !contacts.isEmpty {
if hasContacts {
result.append(.SectionHeader(presentationData: state.presentationData, displayHide: !view.items.isEmpty))
}
}

View File

@ -13,6 +13,7 @@ import AccountContext
import MergedAvatarsNode
import TextNodeWithEntities
import TextFormat
import AvatarNode
class ChatListNoticeItem: ListViewItem {
enum Action {
@ -94,6 +95,7 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode {
private let arrowNode: ASImageNode
private let separatorNode: ASDisplayNode
private var avatarNode: AvatarNode?
private var avatarsNode: MergedAvatarsNode?
private var closeButton: HighlightableButtonNode?
@ -175,6 +177,7 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode {
let titleString: NSAttributedString
let textString: NSAttributedString
var avatarPeer: EnginePeer?
var avatarPeers: [EnginePeer] = []
var okButtonLayout: (TextNodeLayout, () -> TextNode)?
@ -285,12 +288,20 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode {
}
titleString = attributedTitle
textString = NSAttributedString(string: text, font: smallTextFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
case let .setupPhoto(accountPeer):
//TODO:localize
titleString = NSAttributedString(string: "Add your photo! 📸", font: titleFont, textColor: item.theme.rootController.navigationBar.primaryTextColor)
textString = NSAttributedString(string: "Help your friends spot you easily.", font: smallTextFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor)
avatarPeer = accountPeer
}
var leftInset: CGFloat = sideInset
if !avatarPeers.isEmpty {
let avatarsWidth = 30.0 + CGFloat(avatarPeers.count - 1) * 16.0
leftInset += avatarsWidth + 4.0
} else if let _ = avatarPeer {
let avatarsWidth: CGFloat = 40.0
leftInset += avatarsWidth + 6.0
}
let titleLayout = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - titleRightInset, height: 100.0), alignment: alignment, lineSpacing: 0.18))
@ -349,6 +360,25 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode {
strongSelf.avatarsNode = nil
}
if let avatarPeer {
let avatarNode: AvatarNode
if let current = strongSelf.avatarNode {
avatarNode = current
} else {
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 13.0))
avatarNode.isUserInteractionEnabled = false
strongSelf.addSubnode(avatarNode)
strongSelf.avatarNode = avatarNode
avatarNode.setPeer(context: item.context, theme: item.theme, peer: avatarPeer, overrideImage: .cameraIcon)
}
let avatarSize = CGSize(width: 40.0, height: 40.0)
avatarNode.frame = CGRect(origin: CGPoint(x: sideInset - 6.0, y: floor((layout.size.height - avatarSize.height) / 2.0)), size: avatarSize)
} else if let avatarNode = strongSelf.avatarNode {
avatarNode.removeFromSupernode()
strongSelf.avatarNode = nil
}
if let image = strongSelf.arrowNode.image {
strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: layout.size.width - sideInset - image.size.width + 8.0, y: floor((layout.size.height - image.size.height) / 2.0)), size: image.size)
}

View File

@ -1277,9 +1277,20 @@ public final class ChatPresentationInterfaceState: Equatable {
}
}
public func canBypassRestrictions(chatPresentationInterfaceState: ChatPresentationInterfaceState) -> Bool {
guard let boostsToUnrestrict = chatPresentationInterfaceState.boostsToUnrestrict, boostsToUnrestrict > 0 else {
return false
}
if let appliedBoosts = chatPresentationInterfaceState.appliedBoosts, appliedBoosts >= boostsToUnrestrict {
return true
}
return false
}
public func canSendMessagesToChat(_ state: ChatPresentationInterfaceState) -> Bool {
if let peer = state.renderedPeer?.peer {
if canSendMessagesToPeer(peer) {
let canBypassRestrictions = canBypassRestrictions(chatPresentationInterfaceState: state)
if canSendMessagesToPeer(peer, ignoreDefault: canBypassRestrictions) {
return true
} else {
return false

View File

@ -68,6 +68,7 @@ public enum ContactsPeerItemPeerMode: Equatable {
case generalSearch(isSavedMessages: Bool)
case peer
case memberList
case app(isPopular: Bool)
}
public enum ContactsPeerItemBadgeType {
@ -722,7 +723,15 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
let titleBoldFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize)
let statusFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 13.0 / 17.0))
let statusFontSize: CGFloat
if case .app = item.peerMode {
statusFontSize = 15.0
} else {
statusFontSize = 13.0
}
let statusFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * statusFontSize / 17.0))
let badgeFont = Font.regular(14.0)
let avatarDiameter = min(40.0, floor(item.presentationData.fontSize.itemListBaseFontSize * 40.0 / 17.0))
@ -1039,8 +1048,10 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
}
}
let titleVerticalInset: CGFloat = statusAttributedString == nil ? 13.0 : 6.0
let verticalInset: CGFloat = statusAttributedString == nil ? 13.0 : 6.0
var verticalInset: CGFloat = statusAttributedString == nil ? 13.0 : 6.0
if case .app = item.peerMode {
verticalInset += 2.0
}
let statusHeightComponent: CGFloat
if statusAttributedString == nil {
@ -1058,7 +1069,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
let titleFrame: CGRect
if statusAttributedString != nil {
titleFrame = CGRect(origin: CGPoint(x: leftInset, y: titleVerticalInset), size: titleLayout.size)
titleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size)
} else {
titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((nodeLayout.contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size)
}
@ -1136,14 +1147,19 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
} else if peer.isDeleted {
overrideImage = .deletedIcon
}
var displayDimensions = CGSize(width: 60.0, height: 60.0)
let clipStyle: AvatarNodeClipStyle
if case let .channel(channel) = peer, channel.flags.contains(.isForum) {
if case .app(true) = item.peerMode {
clipStyle = .roundedRect
displayDimensions = CGSize(width: displayDimensions.width, height: displayDimensions.width * 1.2)
} else if case let .channel(channel) = peer, channel.flags.contains(.isForum) {
clipStyle = .roundedRect
} else {
clipStyle = .round
}
strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, clipStyle: clipStyle, synchronousLoad: synchronousLoads)
strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: overrideImage, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, clipStyle: clipStyle, synchronousLoad: synchronousLoads, displayDimensions: displayDimensions)
}
case let .deviceContact(_, contact):
let letters: [String]
@ -1230,7 +1246,14 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
}
}
let avatarFrame = CGRect(origin: CGPoint(x: revealOffset + leftInset - 50.0, y: floor((nodeLayout.contentSize.height - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter))
var avatarSize: CGSize
if case .app(true) = item.peerMode {
avatarSize = CGSize(width: avatarDiameter, height: avatarDiameter * 1.2)
} else {
avatarSize = CGSize(width: avatarDiameter, height: avatarDiameter)
}
let avatarFrame = CGRect(origin: CGPoint(x: revealOffset + leftInset - 50.0, y: floor((nodeLayout.contentSize.height - avatarSize.height) / 2.0)), size: avatarSize)
strongSelf.avatarNode.frame = CGRect(origin: CGPoint(), size: avatarFrame.size)

View File

@ -97,7 +97,11 @@ final class NavigationTransitionCoordinator {
case .Push:
self.container.addSubnode(topNode)
case .Pop:
self.container.insertSubnode(bottomNode, belowSubnode: topNode)
if topNode.supernode == self.container {
self.container.insertSubnode(bottomNode, belowSubnode: topNode)
} else {
self.container.addSubnode(topNode)
}
}
if !self.isFlat {

View File

@ -477,6 +477,10 @@ public struct MessageFlags: OptionSet {
rawValue |= MessageFlags.IsForumTopic.rawValue
}
if flags.contains(StoreMessageFlags.ReactionsArePossible) {
rawValue |= MessageFlags.ReactionsArePossible.rawValue
}
self.rawValue = rawValue
}
@ -490,6 +494,7 @@ public struct MessageFlags: OptionSet {
public static let CountedAsIncoming = MessageFlags(rawValue: 256)
public static let CopyProtected = MessageFlags(rawValue: 512)
public static let IsForumTopic = MessageFlags(rawValue: 1024)
public static let ReactionsArePossible = MessageFlags(rawValue: 2048)
public static let IsIncomingMask = MessageFlags([.Incoming, .CountedAsIncoming])
}
@ -843,6 +848,10 @@ public struct StoreMessageFlags: OptionSet {
rawValue |= StoreMessageFlags.IsForumTopic.rawValue
}
if flags.contains(.ReactionsArePossible) {
rawValue |= StoreMessageFlags.ReactionsArePossible.rawValue
}
self.rawValue = rawValue
}
@ -856,6 +865,7 @@ public struct StoreMessageFlags: OptionSet {
public static let CountedAsIncoming = StoreMessageFlags(rawValue: 256)
public static let CopyProtected = StoreMessageFlags(rawValue: 512)
public static let IsForumTopic = StoreMessageFlags(rawValue: 1024)
public static let ReactionsArePossible = StoreMessageFlags(rawValue: 2048)
public static let IsIncomingMask = StoreMessageFlags([.Incoming, .CountedAsIncoming])
}

View File

@ -231,6 +231,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollView
}, dismissNotice: { _ in
}, editPeer: { _ in
}, openWebApp: { _ in
}, openPhotoSetup: {
})
let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true)

View File

@ -380,6 +380,7 @@ final class ThemePreviewControllerNode: ASDisplayNode, ASScrollViewDelegate {
}, dismissNotice: { _ in
}, editPeer: { _ in
}, openWebApp: { _ in
}, openPhotoSetup: {
})
func makeChatListItem(

View File

@ -537,7 +537,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-808853502] = { return Api.MediaAreaCoordinates.parse_mediaAreaCoordinates($0) }
dict[-1808510398] = { return Api.Message.parse_message($0) }
dict[-1868117372] = { return Api.Message.parse_messageEmpty($0) }
dict[721967202] = { return Api.Message.parse_messageService($0) }
dict[-741178048] = { return Api.Message.parse_messageService($0) }
dict[-872240531] = { return Api.MessageAction.parse_messageActionBoostApply($0) }
dict[-988359047] = { return Api.MessageAction.parse_messageActionBotAllowed($0) }
dict[-1781355374] = { return Api.MessageAction.parse_messageActionChannelCreate($0) }

View File

@ -62,7 +62,7 @@ public extension Api {
indirect enum Message: TypeConstructorDescription {
case message(flags: Int32, flags2: Int32, id: Int32, fromId: Api.Peer?, fromBoostsApplied: Int32?, peerId: Api.Peer, savedPeerId: Api.Peer?, fwdFrom: Api.MessageFwdHeader?, viaBotId: Int64?, viaBusinessBotId: Int64?, replyTo: Api.MessageReplyHeader?, date: Int32, message: String, media: Api.MessageMedia?, replyMarkup: Api.ReplyMarkup?, entities: [Api.MessageEntity]?, views: Int32?, forwards: Int32?, replies: Api.MessageReplies?, editDate: Int32?, postAuthor: String?, groupedId: Int64?, reactions: Api.MessageReactions?, restrictionReason: [Api.RestrictionReason]?, ttlPeriod: Int32?, quickReplyShortcutId: Int32?, effect: Int64?, factcheck: Api.FactCheck?)
case messageEmpty(flags: Int32, id: Int32, peerId: Api.Peer?)
case messageService(flags: Int32, id: Int32, fromId: Api.Peer?, peerId: Api.Peer, replyTo: Api.MessageReplyHeader?, date: Int32, action: Api.MessageAction, ttlPeriod: Int32?)
case messageService(flags: Int32, id: Int32, fromId: Api.Peer?, peerId: Api.Peer, replyTo: Api.MessageReplyHeader?, date: Int32, action: Api.MessageAction, reactions: Api.MessageReactions?, ttlPeriod: Int32?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
@ -115,9 +115,9 @@ public extension Api {
serializeInt32(id, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {peerId!.serialize(buffer, true)}
break
case .messageService(let flags, let id, let fromId, let peerId, let replyTo, let date, let action, let ttlPeriod):
case .messageService(let flags, let id, let fromId, let peerId, let replyTo, let date, let action, let reactions, let ttlPeriod):
if boxed {
buffer.appendInt32(721967202)
buffer.appendInt32(-741178048)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt32(id, buffer: buffer, boxed: false)
@ -126,6 +126,7 @@ public extension Api {
if Int(flags) & Int(1 << 3) != 0 {replyTo!.serialize(buffer, true)}
serializeInt32(date, buffer: buffer, boxed: false)
action.serialize(buffer, true)
if Int(flags) & Int(1 << 20) != 0 {reactions!.serialize(buffer, true)}
if Int(flags) & Int(1 << 25) != 0 {serializeInt32(ttlPeriod!, buffer: buffer, boxed: false)}
break
}
@ -137,8 +138,8 @@ public extension Api {
return ("message", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("fromId", fromId as Any), ("fromBoostsApplied", fromBoostsApplied as Any), ("peerId", peerId as Any), ("savedPeerId", savedPeerId as Any), ("fwdFrom", fwdFrom as Any), ("viaBotId", viaBotId as Any), ("viaBusinessBotId", viaBusinessBotId as Any), ("replyTo", replyTo as Any), ("date", date as Any), ("message", message as Any), ("media", media as Any), ("replyMarkup", replyMarkup as Any), ("entities", entities as Any), ("views", views as Any), ("forwards", forwards as Any), ("replies", replies as Any), ("editDate", editDate as Any), ("postAuthor", postAuthor as Any), ("groupedId", groupedId as Any), ("reactions", reactions as Any), ("restrictionReason", restrictionReason as Any), ("ttlPeriod", ttlPeriod as Any), ("quickReplyShortcutId", quickReplyShortcutId as Any), ("effect", effect as Any), ("factcheck", factcheck as Any)])
case .messageEmpty(let flags, let id, let peerId):
return ("messageEmpty", [("flags", flags as Any), ("id", id as Any), ("peerId", peerId as Any)])
case .messageService(let flags, let id, let fromId, let peerId, let replyTo, let date, let action, let ttlPeriod):
return ("messageService", [("flags", flags as Any), ("id", id as Any), ("fromId", fromId as Any), ("peerId", peerId as Any), ("replyTo", replyTo as Any), ("date", date as Any), ("action", action as Any), ("ttlPeriod", ttlPeriod as Any)])
case .messageService(let flags, let id, let fromId, let peerId, let replyTo, let date, let action, let reactions, let ttlPeriod):
return ("messageService", [("flags", flags as Any), ("id", id as Any), ("fromId", fromId as Any), ("peerId", peerId as Any), ("replyTo", replyTo as Any), ("date", date as Any), ("action", action as Any), ("reactions", reactions as Any), ("ttlPeriod", ttlPeriod as Any)])
}
}
@ -300,8 +301,12 @@ public extension Api {
if let signature = reader.readInt32() {
_7 = Api.parse(reader, signature: signature) as? Api.MessageAction
}
var _8: Int32?
if Int(_1!) & Int(1 << 25) != 0 {_8 = reader.readInt32() }
var _8: Api.MessageReactions?
if Int(_1!) & Int(1 << 20) != 0 {if let signature = reader.readInt32() {
_8 = Api.parse(reader, signature: signature) as? Api.MessageReactions
} }
var _9: Int32?
if Int(_1!) & Int(1 << 25) != 0 {_9 = reader.readInt32() }
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = (Int(_1!) & Int(1 << 8) == 0) || _3 != nil
@ -309,9 +314,10 @@ public extension Api {
let _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil
let _c6 = _6 != nil
let _c7 = _7 != nil
let _c8 = (Int(_1!) & Int(1 << 25) == 0) || _8 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 {
return Api.Message.messageService(flags: _1!, id: _2!, fromId: _3, peerId: _4!, replyTo: _5, date: _6!, action: _7!, ttlPeriod: _8)
let _c8 = (Int(_1!) & Int(1 << 20) == 0) || _8 != nil
let _c9 = (Int(_1!) & Int(1 << 25) == 0) || _9 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 {
return Api.Message.messageService(flags: _1!, id: _2!, fromId: _3, peerId: _4!, replyTo: _5, date: _6!, action: _7!, reactions: _8, ttlPeriod: _9)
}
else {
return nil

View File

@ -135,7 +135,7 @@ func apiMessagePeerId(_ messsage: Api.Message) -> PeerId? {
} else {
return nil
}
case let .messageService(_, _, _, chatPeerId, _, _, _, _):
case let .messageService(_, _, _, chatPeerId, _, _, _, _, _):
return chatPeerId.peerId
}
}
@ -216,7 +216,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] {
return result
case .messageEmpty:
return []
case let .messageService(_, _, fromId, chatPeerId, _, _, action, _):
case let .messageService(_, _, fromId, chatPeerId, _, _, action, _, _):
let peerId: PeerId = chatPeerId.peerId
var result = [peerId]
@ -295,7 +295,7 @@ func apiMessageAssociatedMessageIds(_ message: Api.Message) -> (replyIds: Refere
}
case .messageEmpty:
break
case let .messageService(_, id, _, chatPeerId, replyHeader, _, _, _):
case let .messageService(_, id, _, chatPeerId, replyHeader, _, _, _, _):
if let replyHeader = replyHeader {
switch replyHeader {
case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, replyMedia, replyToTopId, quoteText, quoteEntities, quoteOffset):
@ -1015,7 +1015,7 @@ extension StoreMessage {
self.init(id: MessageId(peerId: peerId, namespace: namespace, id: id), globallyUniqueId: nil, groupingKey: groupingId, threadId: threadId, timestamp: date, flags: storeFlags, tags: tags, globalTags: globalTags, localTags: [], forwardInfo: forwardInfo, authorId: authorId, text: messageText, attributes: attributes, media: medias)
case .messageEmpty:
return nil
case let .messageService(flags, id, fromId, chatPeerId, replyTo, date, action, ttlPeriod):
case let .messageService(flags, id, fromId, chatPeerId, replyTo, date, action, reactions, ttlPeriod):
let peerId: PeerId = chatPeerId.peerId
let authorId: PeerId? = fromId?.peerId ?? chatPeerId.peerId
@ -1074,6 +1074,10 @@ extension StoreMessage {
if (flags & (1 << 17)) != 0 {
attributes.append(ContentRequiresValidationMessageAttribute())
}
if let reactions = reactions {
attributes.append(ReactionsMessageAttribute(apiReactions: reactions))
}
var storeFlags = StoreMessageFlags()
if (flags & 2) == 0 {
@ -1120,6 +1124,10 @@ extension StoreMessage {
if (flags & (1 << 27)) != 0 {
storeFlags.insert(.IsForumTopic)
}
if (flags & (1 << 9)) != 0 {
storeFlags.insert(.ReactionsArePossible)
}
self.init(id: MessageId(peerId: peerId, namespace: namespace, id: id), globallyUniqueId: nil, groupingKey: nil, threadId: threadId, timestamp: date, flags: storeFlags, tags: tags, globalTags: globalTags, localTags: [], forwardInfo: nil, authorId: authorId, text: "", attributes: attributes, media: media)
}

View File

@ -24,7 +24,7 @@ public enum TelegramChannelPermission {
}
public extension TelegramChannel {
func hasPermission(_ permission: TelegramChannelPermission) -> Bool {
func hasPermission(_ permission: TelegramChannelPermission, ignoreDefault: Bool = false) -> Bool {
if self.flags.contains(.isCreator) {
if case .canBeAnonymous = permission {
if let adminRights = self.adminRights {
@ -50,7 +50,7 @@ public extension TelegramChannel {
if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banSendText) {
return false
}
if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banSendText) {
if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banSendText) && !ignoreDefault {
return false
}
return true
@ -69,7 +69,7 @@ public extension TelegramChannel {
if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banSendPhotos) {
return false
}
if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banSendPhotos) {
if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banSendPhotos) && !ignoreDefault {
return false
}
return true
@ -88,7 +88,7 @@ public extension TelegramChannel {
if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banSendVideos) {
return false
}
if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banSendVideos) {
if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banSendVideos) && !ignoreDefault {
return false
}
return true
@ -121,7 +121,7 @@ public extension TelegramChannel {
if let bannedRights = self.bannedRights, bannedRights.flags.intersection(flags) == flags {
return false
}
if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.intersection(flags) == flags {
if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.intersection(flags) == flags && !ignoreDefault {
return false
}
return true

View File

@ -108,7 +108,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes
updatedTimestamp = date
case .messageEmpty:
break
case let .messageService(_, _, _, _, _, date, _, _):
case let .messageService(_, _, _, _, _, date, _, _, _):
updatedTimestamp = date
}
} else {

View File

@ -210,7 +210,7 @@ public class BoxedMessage: NSObject {
public class Serialization: NSObject, MTSerialization {
public func currentLayer() -> UInt {
return 195
return 196
}
public func parseMessage(_ data: Data!) -> Any! {

View File

@ -108,7 +108,7 @@ extension Api.Message {
return id
case let .messageEmpty(_, id, _):
return id
case let .messageService(_, id, _, _, _, _, _, _):
case let .messageService(_, id, _, _, _, _, _, _, _):
return id
}
}
@ -128,7 +128,7 @@ extension Api.Message {
} else {
return nil
}
case let .messageService(_, id, _, chatPeerId, _, _, _, _):
case let .messageService(_, id, _, chatPeerId, _, _, _, _, _):
let peerId: PeerId = chatPeerId.peerId
return MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: id)
}
@ -141,7 +141,7 @@ extension Api.Message {
return peerId
case let .messageEmpty(_, _, peerId):
return peerId?.peerId
case let .messageService(_, _, _, chatPeerId, _, _, _, _):
case let .messageService(_, _, _, chatPeerId, _, _, _, _, _):
let peerId: PeerId = chatPeerId.peerId
return peerId
}
@ -151,7 +151,7 @@ extension Api.Message {
switch self {
case let .message(_, _, _, _, _, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
return date
case let .messageService(_, _, _, _, _, date, _, _):
case let .messageService(_, _, _, _, _, date, _, _, _):
return date
case .messageEmpty:
return nil

View File

@ -17,6 +17,7 @@ public enum ServerProvidedSuggestion: String {
case todayBirthdays = "BIRTHDAY_CONTACTS_TODAY"
case gracePremium = "PREMIUM_GRACE"
case starsSubscriptionLowBalance = "STARS_SUBSCRIPTION_LOW_BALANCE"
case setupPhoto = "USERPIC_SETUP"
}
private var dismissedSuggestionsPromise = ValuePromise<[AccountRecordId: Set<ServerProvidedSuggestion>]>([:])
@ -40,7 +41,6 @@ func _internal_getServerProvidedSuggestions(account: Account) -> Signal<[ServerP
guard let data = appConfiguration.data, let listItems = data["pending_suggestions"] as? [String] else {
return []
}
return listItems.compactMap { item -> ServerProvidedSuggestion? in
return ServerProvidedSuggestion(rawValue: item)
}.filter { !dismissedSuggestions.contains($0) }

View File

@ -6,7 +6,7 @@ import Postbox
private final class LinkHelperClass: NSObject {
}
public func canSendMessagesToPeer(_ peer: Peer) -> Bool {
public func canSendMessagesToPeer(_ peer: Peer, ignoreDefault: Bool = false) -> Bool {
if let peer = peer as? TelegramUser, peer.addressName == "replies" {
return false
} else if peer is TelegramUser || peer is TelegramGroup {
@ -14,7 +14,7 @@ public func canSendMessagesToPeer(_ peer: Peer) -> Bool {
} else if let peer = peer as? TelegramSecretChat {
return peer.embeddedState == .active
} else if let peer = peer as? TelegramChannel {
return peer.hasPermission(.sendSomething)
return peer.hasPermission(.sendSomething, ignoreDefault: ignoreDefault)
} else {
return false
}

View File

@ -671,6 +671,8 @@ public final class ChatInlineSearchResultsListComponent: Component {
editPeer: { _ in
},
openWebApp: { _ in
},
openPhotoSetup: {
}
)
self.chatListNodeInteraction = chatListNodeInteraction

View File

@ -230,7 +230,8 @@ public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
var backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: labelLayout.size.height + 4.0)
if let _ = image {
backgroundSize.height += imageSize.height + 10
backgroundSize.width = imageSize.width + 2.0
backgroundSize.height += imageSize.height + 10.0
}
return (backgroundSize.width, { boundingWidth in
return (backgroundSize, { [weak self] animation, synchronousLoads, _ in
@ -239,7 +240,7 @@ public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
let maskPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: imageSize), cornerRadius: 15.5)
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - imageSize.width) / 2.0), y: labelLayout.size.height + 10 + 2), size: imageSize)
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - imageSize.width) / 2.0), y: labelLayout.size.height + 12.0), size: imageSize)
if let image = image {
let imageNode: TransformImageNode
if let current = strongSelf.imageNode {
@ -319,7 +320,7 @@ public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
attemptSynchronous: synchronousLoads
))
let labelFrame = CGRect(origin: CGPoint(x: 8.0, y: image != nil ? 2 : floorToScreenPixels((backgroundSize.height - labelLayout.size.height) / 2.0) - 1.0), size: labelLayout.size)
let labelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - labelLayout.size.width) / 2.0) - 1.0, y: image != nil ? 2.0 : floorToScreenPixels((backgroundSize.height - labelLayout.size.height) / 2.0) - 1.0), size: labelLayout.size)
if story != nil {
let leadingIconView: UIImageView

View File

@ -1301,7 +1301,8 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
associatedData: item.associatedData,
accountPeer: item.associatedData.accountPeer,
isIncoming: item.message.effectivelyIncoming(item.context.account.peerId),
constrainedWidth: maxReactionsWidth
constrainedWidth: maxReactionsWidth,
centerAligned: false
))
maxContentWidth = max(maxContentWidth, minWidth)
reactionButtonsFinalize = buttonsLayout

View File

@ -208,6 +208,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
isAction = true
if case .phoneCall = action.action {
result.append((message, ChatMessageCallBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
needReactions = false
} else if case .giftPremium = action.action {
result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
} else if case .giftStars = action.action {
@ -225,10 +226,16 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
} else if case .joinedChannel = action.action {
result.append((message, ChatMessageJoinedChannelBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
needReactions = false
} else {
switch action.action {
case .photoUpdated:
break
default:
needReactions = false
}
result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
}
needReactions = false
} else if let _ = media as? TelegramMediaMap {
result.append((message, ChatMessageMapBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .media, neighborSpacing: .default)))
} else if let _ = media as? TelegramMediaGame {
@ -2700,6 +2707,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
var reactionButtonsFinalize: ((CGFloat) -> (CGSize, (_ animation: ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode))?
if !bubbleReactions.reactions.isEmpty && !item.presentationData.isPreview {
var centerAligned = false
for media in item.message.media {
if media is TelegramMediaAction {
centerAligned = true
}
break
}
var maximumNodeWidth = maximumNodeWidth
if hasInstantVideo {
maximumNodeWidth = min(309, baseWidth - 84)
@ -2715,7 +2730,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
associatedData: item.associatedData,
accountPeer: item.associatedData.accountPeer,
isIncoming: incoming,
constrainedWidth: maximumNodeWidth
constrainedWidth: maximumNodeWidth,
centerAligned: centerAligned
))
maxContentWidth = max(maxContentWidth, minWidth)
reactionButtonsFinalize = buttonsLayout

View File

@ -616,7 +616,8 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, ASGestureReco
associatedData: item.associatedData,
accountPeer: item.associatedData.accountPeer,
isIncoming: item.message.effectivelyIncoming(item.context.account.peerId),
constrainedWidth: maxReactionsWidth
constrainedWidth: maxReactionsWidth,
centerAligned: false
))
maxContentWidth = max(maxContentWidth, minWidth)
reactionButtonsFinalize = buttonsLayout

View File

@ -285,7 +285,7 @@ public func canAddMessageReactions(message: Message) -> Bool {
}
for media in message.media {
if let _ = media as? TelegramMediaAction {
return false
return message.flags.contains(.ReactionsArePossible)
} else if let story = media as? TelegramMediaStory {
if story.isMention {
return false

View File

@ -30,6 +30,7 @@ public final class MessageReactionButtonsNode: ASDisplayNode {
public enum DisplayAlignment {
case left
case right
case center
}
private var bubbleBackgroundNode: WallpaperBubbleBackgroundNode?
@ -224,27 +225,29 @@ public final class MessageReactionButtonsNode: ASDisplayNode {
constrainedWidth: constrainedWidth
)
let itemSpacing: CGFloat = 6.0
var reactionButtonsSize = CGSize()
var currentRowWidth: CGFloat = 0.0
for item in reactionButtonsResult.items {
if currentRowWidth + item.size.width > constrainedWidth {
reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth)
if !reactionButtonsSize.height.isZero {
reactionButtonsSize.height += 6.0
reactionButtonsSize.height += itemSpacing
}
reactionButtonsSize.height += item.size.height
currentRowWidth = 0.0
}
if !currentRowWidth.isZero {
currentRowWidth += 6.0
currentRowWidth += itemSpacing
}
currentRowWidth += item.size.width
}
if !currentRowWidth.isZero && !reactionButtonsResult.items.isEmpty {
reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth)
if !reactionButtonsSize.height.isZero {
reactionButtonsSize.height += 6.0
reactionButtonsSize.height += itemSpacing
}
reactionButtonsSize.height += reactionButtonsResult.items[0].size.height
}
@ -296,11 +299,14 @@ public final class MessageReactionButtonsNode: ASDisplayNode {
}
var reactionButtonPosition: CGPoint
switch alignment {
case .left:
reactionButtonPosition = CGPoint(x: -1.0, y: topInset)
case .right:
reactionButtonPosition = CGPoint(x: size.width + 1.0, y: topInset)
case .center:
reactionButtonPosition = CGPoint(x: 0.0, y: topInset)
}
let reactionButtons = reactionButtonsResult.apply(
@ -312,32 +318,8 @@ public final class MessageReactionButtonsNode: ASDisplayNode {
)
var validIds = Set<MessageReaction.Reaction>()
for item in reactionButtons.items {
validIds.insert(item.value)
switch alignment {
case .left:
if reactionButtonPosition.x + item.size.width > boundingWidth {
reactionButtonPosition.x = -1.0
reactionButtonPosition.y += item.size.height + 6.0
}
case .right:
if reactionButtonPosition.x - item.size.width < -1.0 {
reactionButtonPosition.x = size.width + 1.0
reactionButtonPosition.y += item.size.height + 6.0
}
}
let itemFrame: CGRect
switch alignment {
case .left:
itemFrame = CGRect(origin: reactionButtonPosition, size: item.size)
reactionButtonPosition.x += item.size.width + 6.0
case .right:
itemFrame = CGRect(origin: CGPoint(x: reactionButtonPosition.x - item.size.width, y: reactionButtonPosition.y), size: item.size)
reactionButtonPosition.x -= item.size.width + 6.0
}
let layoutItem: (ReactionButtonsAsyncLayoutContainer.ApplyResult.Item, CGRect) -> Void = { item, itemFrame in
let itemMaskFrame = itemFrame.offsetBy(dx: backgroundInsets, dy: backgroundInsets)
let itemMaskView: UIView
@ -396,6 +378,77 @@ public final class MessageReactionButtonsNode: ASDisplayNode {
}
}
}
if alignment == .center {
var lines: [[ReactionButtonsAsyncLayoutContainer.ApplyResult.Item]] = []
var currentLine: [ReactionButtonsAsyncLayoutContainer.ApplyResult.Item] = []
var currentLineWidth: CGFloat = 0.0
for item in reactionButtons.items {
validIds.insert(item.value)
let itemWidth = item.size.width
if currentLineWidth + itemWidth + (currentLine.isEmpty ? 0 : itemSpacing) > boundingWidth + itemSpacing {
lines.append(currentLine)
currentLine = [item]
currentLineWidth = itemWidth
} else {
currentLine.append(item)
currentLineWidth += (currentLine.isEmpty ? 0 : itemSpacing) + itemWidth
}
}
if !currentLine.isEmpty {
lines.append(currentLine)
}
var yPosition = topInset
for line in lines {
let totalItemWidth = line.reduce(0.0) { $0 + $1.size.width } + CGFloat(line.count - 1) * itemSpacing
let startX = (boundingWidth - totalItemWidth) / 2.0
var xPosition = startX
for item in line {
let itemFrame = CGRect(origin: CGPoint(x: xPosition, y: yPosition), size: item.size)
xPosition += item.size.width + itemSpacing
layoutItem(item, itemFrame)
}
yPosition += line.first!.size.height + itemSpacing
}
} else {
for item in reactionButtons.items {
validIds.insert(item.value)
switch alignment {
case .left:
if reactionButtonPosition.x + item.size.width > boundingWidth {
reactionButtonPosition.x = -1.0
reactionButtonPosition.y += item.size.height + itemSpacing
}
case .right:
if reactionButtonPosition.x - item.size.width < -1.0 {
reactionButtonPosition.x = size.width + 1.0
reactionButtonPosition.y += item.size.height + itemSpacing
}
default:
break
}
let itemFrame: CGRect
switch alignment {
case .left, .center:
itemFrame = CGRect(origin: reactionButtonPosition, size: item.size)
reactionButtonPosition.x += item.size.width + itemSpacing
case .right:
itemFrame = CGRect(origin: CGPoint(x: reactionButtonPosition.x - item.size.width, y: reactionButtonPosition.y), size: item.size)
reactionButtonPosition.x -= item.size.width + itemSpacing
}
layoutItem(item, itemFrame)
}
}
var removeMaskIds: [MessageReaction.Reaction] = []
for (id, view) in strongSelf.backgroundMaskButtons {
@ -629,6 +682,7 @@ public final class ChatMessageReactionButtonsNode: ASDisplayNode {
public let accountPeer: EnginePeer?
public let isIncoming: Bool
public let constrainedWidth: CGFloat
public let centerAligned: Bool
public init(
context: AccountContext,
@ -641,7 +695,8 @@ public final class ChatMessageReactionButtonsNode: ASDisplayNode {
associatedData: ChatMessageItemAssociatedData,
accountPeer: EnginePeer?,
isIncoming: Bool,
constrainedWidth: CGFloat
constrainedWidth: CGFloat,
centerAligned: Bool
) {
self.context = context
self.presentationData = presentationData
@ -654,6 +709,7 @@ public final class ChatMessageReactionButtonsNode: ASDisplayNode {
self.accountPeer = accountPeer
self.isIncoming = isIncoming
self.constrainedWidth = constrainedWidth
self.centerAligned = centerAligned
}
}
@ -682,6 +738,13 @@ public final class ChatMessageReactionButtonsNode: ASDisplayNode {
return { arguments in
let node = maybeNode ?? ChatMessageReactionButtonsNode()
let alignment: MessageReactionButtonsNode.DisplayAlignment
if arguments.centerAligned {
alignment = .center
} else {
alignment = arguments.isIncoming ? .left : .right
}
let buttonsUpdate = node.buttonsNode.prepareUpdate(
context: arguments.context,
presentationData: arguments.presentationData,
@ -692,7 +755,7 @@ public final class ChatMessageReactionButtonsNode: ASDisplayNode {
accountPeer: arguments.accountPeer,
message: arguments.message,
associatedData: arguments.associatedData,
alignment: arguments.isIncoming ? .left : .right,
alignment: alignment,
constrainedWidth: arguments.constrainedWidth,
type: .freeform
)

View File

@ -432,8 +432,8 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
buttons = [
self.deleteButton,
tagButton,
self.forwardButton,
self.shareButton
self.shareButton,
self.forwardButton
]
} else {
buttons = [

View File

@ -863,7 +863,8 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
associatedData: item.associatedData,
accountPeer: item.associatedData.accountPeer,
isIncoming: item.message.effectivelyIncoming(item.context.account.peerId),
constrainedWidth: maxReactionsWidth
constrainedWidth: maxReactionsWidth,
centerAligned: false
))
maxContentWidth = max(maxContentWidth, minWidth)
reactionButtonsFinalize = buttonsLayout

View File

@ -631,6 +631,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, openAgeRestrictedMessageMedia: { _, _ in
}, playMessageEffect: { _ in
}, editMessageFactCheck: { _ in
}, sendGift: { _ in
}, requestMessageUpdate: { _, _ in
}, cancelInteractiveKeyboardGestures: {

View File

@ -488,6 +488,7 @@ public final class ChatSendGroupMediaMessageContextPreview: UIView, ChatSendMess
}, openAgeRestrictedMessageMedia: { _, _ in
}, playMessageEffect: { _ in
}, editMessageFactCheck: { _ in
}, sendGift: { _ in
}, requestMessageUpdate: { _, _ in
}, cancelInteractiveKeyboardGestures: {
}, dismissTextInput: {

View File

@ -267,6 +267,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
public let openAgeRestrictedMessageMedia: (Message, @escaping () -> Void) -> Void
public let playMessageEffect: (Message) -> Void
public let editMessageFactCheck: (MessageId) -> Void
public let sendGift: (EnginePeer.Id) -> Void
public let requestMessageUpdate: (MessageId, Bool) -> Void
public let cancelInteractiveKeyboardGestures: () -> Void
@ -400,6 +401,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
openAgeRestrictedMessageMedia: @escaping (Message, @escaping () -> Void) -> Void,
playMessageEffect: @escaping (Message) -> Void,
editMessageFactCheck: @escaping (MessageId) -> Void,
sendGift: @escaping (EnginePeer.Id) -> Void,
requestMessageUpdate: @escaping (MessageId, Bool) -> Void,
cancelInteractiveKeyboardGestures: @escaping () -> Void,
dismissTextInput: @escaping () -> Void,
@ -512,6 +514,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
self.openAgeRestrictedMessageMedia = openAgeRestrictedMessageMedia
self.playMessageEffect = playMessageEffect
self.editMessageFactCheck = editMessageFactCheck
self.sendGift = sendGift
self.requestMessageUpdate = requestMessageUpdate
self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures

View File

@ -406,7 +406,7 @@ final class GiftSetupScreenComponent: Component {
purpose: .starGift(peerId: component.peerId, requiredStars: starGift.price),
completion: { [weak starsContext] stars in
starsContext?.add(balance: StarsAmount(value: stars, nanos: 0))
Queue.mainQueue().after(0.1) {
Queue.mainQueue().after(2.0) {
proceed()
}
}

View File

@ -186,6 +186,7 @@ public final class LoadingOverlayNode: ASDisplayNode {
}, dismissNotice: { _ in
}, editPeer: { _ in
}, openWebApp: { _ in
}, openPhotoSetup: {
})
let items = (0 ..< 1).map { _ -> ChatListItem in
@ -542,6 +543,8 @@ private final class PeerInfoScreenPersonalChannelItemNode: PeerInfoScreenItemNod
editPeer: { _ in
},
openWebApp: { _ in
},
openPhotoSetup: {
}
)

View File

@ -3621,6 +3621,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}, openAgeRestrictedMessageMedia: { _, _ in
}, playMessageEffect: { _ in
}, editMessageFactCheck: { _ in
}, sendGift: { _ in
}, requestMessageUpdate: { _, _ in
}, cancelInteractiveKeyboardGestures: {
}, dismissTextInput: {
@ -9788,7 +9789,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
}))
}
fileprivate func openAvatarForEditing(mode: PeerInfoAvatarEditingMode = .generic, fromGallery: Bool = false, completion: @escaping (UIImage?) -> Void = { _ in }) {
guard let peer = self.data?.peer, mode != .generic || canEditPeerInfo(context: self.context, peer: peer, chatLocation: self.chatLocation, threadData: self.data?.threadData) else {
return
@ -9825,7 +9826,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
legacyController.bind(controller: navigationController)
strongSelf.view.endEditing(true)
(strongSelf.controller?.navigationController?.topViewController as? ViewController)?.present(legacyController, in: .window(.root))
let parentController = (strongSelf.context.sharedContext.mainWindow?.viewController as? NavigationController)?.topViewController as? ViewController
parentController?.present(legacyController, in: .window(.root))
var hasPhotos = false
if !peer.profileImageRepresentations.isEmpty {
@ -9876,7 +9880,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: hasDeleteButton, hasViewButton: false, personalPhoto: strongSelf.isSettings || strongSelf.isMyProfile, isVideo: currentIsVideo, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: isForum, title: title, isSuggesting: [.custom, .suggest].contains(mode))!
mixin.stickersContext = LegacyPaintStickersContext(context: strongSelf.context)
let _ = strongSelf.currentAvatarMixin.swap(mixin)
mixin.requestSearchController = { [weak self] assetsController in
mixin.requestSearchController = { [weak self, weak parentController] assetsController in
guard let strongSelf = self else {
return
}
@ -9885,14 +9889,14 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
self?.updateProfilePhoto(result, mode: mode)
}))
controller.navigationPresentation = .modal
(strongSelf.controller?.navigationController?.topViewController as? ViewController)?.push(controller)
parentController?.push(controller)
if fromGallery {
completion(nil)
}
}
var isFromEditor = false
mixin.requestAvatarEditor = { [weak self] imageCompletion, videoCompletion in
mixin.requestAvatarEditor = { [weak self, weak parentController] imageCompletion, videoCompletion in
guard let strongSelf = self, let imageCompletion, let videoCompletion else {
return
}
@ -9913,27 +9917,33 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
let controller = AvatarEditorScreen(context: strongSelf.context, inputData: keyboardInputData.get(), peerType: peerType, markup: emojiMarkup)
controller.imageCompletion = imageCompletion
controller.videoCompletion = videoCompletion
(strongSelf.controller?.navigationController?.topViewController as? ViewController)?.push(controller)
parentController?.push(controller)
isFromEditor = true
Queue.mainQueue().after(1.0) {
if let rootController = strongSelf.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
rootController.openSettings()
}
}
}
if let confirmationTextPhoto, let confirmationAction {
mixin.willFinishWithImage = { [weak self] image, commit in
mixin.willFinishWithImage = { [weak self, weak parentController] image, commit in
if let strongSelf = self, let image {
let controller = photoUpdateConfirmationController(context: strongSelf.context, peer: peer, image: image, text: confirmationTextPhoto, doneTitle: confirmationAction, commit: {
commit?()
})
(strongSelf.controller?.navigationController?.topViewController as? ViewController)?.presentInGlobalOverlay(controller)
parentController?.presentInGlobalOverlay(controller)
}
}
}
if let confirmationTextVideo, let confirmationAction {
mixin.willFinishWithVideo = { [weak self] image, commit in
mixin.willFinishWithVideo = { [weak self, weak parentController] image, commit in
if let strongSelf = self, let image {
let controller = photoUpdateConfirmationController(context: strongSelf.context, peer: peer, image: image, text: confirmationTextVideo, doneTitle: confirmationAction, isDark: !isFromEditor, commit: {
commit?()
})
(strongSelf.controller?.navigationController?.topViewController as? ViewController)?.presentInGlobalOverlay(controller)
parentController?.presentInGlobalOverlay(controller)
}
}
}
@ -13008,6 +13018,20 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
}
}
public func openAvatarSetup() {
let proceed = { [weak self] in
self?.controllerNode.openAvatarForEditing()
}
if !self.isNodeLoaded {
self.loadDisplayNode()
Queue.mainQueue().after(0.1) {
proceed()
}
} else {
proceed()
}
}
public func updateProfilePhoto(_ image: UIImage, mode: PeerInfoAvatarEditingMode) {
if !self.isNodeLoaded {
self.loadDisplayNode()

View File

@ -209,6 +209,8 @@ final class GreetingMessageListItemComponent: Component {
editPeer: { _ in
},
openWebApp: { _ in
},
openPhotoSetup: {
}
)
self.chatListNodeInteraction = chatListNodeInteraction

View File

@ -230,6 +230,8 @@ final class QuickReplySetupScreenComponent: Component {
}
},
openWebApp: { _ in
},
openPhotoSetup: {
}
)

View File

@ -874,6 +874,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate
}, dismissNotice: { _ in
}, editPeer: { _ in
}, openWebApp: { _ in
}, openPhotoSetup: {
})
let chatListPresentationData = ChatListPresentationData(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true)

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "setavatar.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -2093,7 +2093,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, peer.hasBannedPermission(.banSendStickers) != nil {
if let boostsToUnrestrict = strongSelf.presentationInterfaceState.boostsToUnrestrict, boostsToUnrestrict > 0, (strongSelf.presentationInterfaceState.appliedBoosts ?? 0) < boostsToUnrestrict {
if !canBypassRestrictions(chatPresentationInterfaceState: strongSelf.presentationInterfaceState) {
strongSelf.interfaceInteraction?.openBoostToUnrestrict()
return false
}
@ -2245,7 +2245,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, peer.hasBannedPermission(.banSendGifs) != nil {
if let boostsToUnrestrict = strongSelf.presentationInterfaceState.boostsToUnrestrict, boostsToUnrestrict > 0, (strongSelf.presentationInterfaceState.appliedBoosts ?? 0) < boostsToUnrestrict {
if !canBypassRestrictions(chatPresentationInterfaceState: strongSelf.presentationInterfaceState) {
strongSelf.interfaceInteraction?.openBoostToUnrestrict()
return false
}
@ -2296,7 +2296,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
if let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, peer.hasBannedPermission(.banSendGifs) != nil {
if let boostsToUnrestrict = strongSelf.presentationInterfaceState.boostsToUnrestrict, boostsToUnrestrict > 0, (strongSelf.presentationInterfaceState.appliedBoosts ?? 0) < boostsToUnrestrict {
if !canBypassRestrictions(chatPresentationInterfaceState: strongSelf.presentationInterfaceState) {
strongSelf.interfaceInteraction?.openBoostToUnrestrict()
return false
}
@ -4554,6 +4554,20 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return
}
self.openEditMessageFactCheck(messageId: messageId)
}, sendGift: { [weak self] peerId in
guard let self else {
return
}
let _ = (self.context.engine.payments.premiumGiftCodeOptions(peerId: nil, onlyCached: true)
|> filter { !$0.isEmpty }
|> deliverOnMainQueue).start(next: { [weak self] giftOptions in
guard let self else {
return
}
let premiumOptions = giftOptions.filter { $0.users == 1 }.map { CachedPremiumGiftOption(months: $0.months, currency: $0.currency, amount: $0.amount, botUrl: "", storeProductId: $0.storeProductId) }
let controller = self.context.sharedContext.makeGiftOptionsController(context: context, peerId: peerId, premiumOptions: premiumOptions, hasBirthday: false)
self.push(controller)
})
}, requestMessageUpdate: { [weak self] id, scroll in
if let self {
self.chatDisplayNode.historyNode.requestMessageUpdate(id, andScrollToItem: scroll)

View File

@ -339,7 +339,8 @@ func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceS
case .peer:
if let channel = peer as? TelegramChannel {
if case .member = channel.participationStatus {
canReply = channel.hasPermission(.sendSomething)
let canBypassRestrictions = canBypassRestrictions(chatPresentationInterfaceState: chatPresentationInterfaceState)
canReply = channel.hasPermission(.sendSomething, ignoreDefault: canBypassRestrictions)
}
if case .broadcast = channel.info {
canReply = true
@ -1120,6 +1121,23 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
}
}
if data.messageActions.options.contains(.sendGift) {
//TODO:localize
let sendGiftTitle: String
if message.effectivelyIncoming(context.account.peerId) {
let peerName = message.peers[message.id.peerId].flatMap(EnginePeer.init)?.compactDisplayTitle ?? ""
sendGiftTitle = "Send Gift to \(peerName)"
} else {
sendGiftTitle = "Send Another Gift"
}
actions.append(.action(ContextMenuActionItem(text: sendGiftTitle, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Gift"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
let _ = controllerInteraction.sendGift(message.id.peerId)
f(.dismissWithoutContent)
})))
}
var isReplyThreadHead = false
if case let .replyThread(replyThreadMessage) = chatPresentationInterfaceState.chatLocation {
isReplyThreadHead = messages[0].id == replyThreadMessage.effectiveTopId
@ -2220,8 +2238,15 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer
}
break
}
} else if let action = media as? TelegramMediaAction, case .phoneCall = action.action {
optionsMap[id]!.insert(.rateCall)
} else if let action = media as? TelegramMediaAction {
switch action.action {
case .phoneCall:
optionsMap[id]!.insert(.rateCall)
case .starGift:
optionsMap[id]!.insert(.sendGift)
default:
break
}
} else if let story = media as? TelegramMediaStory {
if let story = message.associatedStories[story.storyId], story.data.isEmpty {
isShareProtected = true

View File

@ -9,16 +9,6 @@ import ChatBotStartInputPanelNode
import ChatChannelSubscriberInputPanelNode
import ChatMessageSelectionInputPanelNode
func canBypassRestrictions(chatPresentationInterfaceState: ChatPresentationInterfaceState) -> Bool {
guard let boostsToUnrestrict = chatPresentationInterfaceState.boostsToUnrestrict else {
return false
}
if let appliedBoosts = chatPresentationInterfaceState.appliedBoosts, appliedBoosts >= boostsToUnrestrict {
return true
}
return false
}
func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentPanel: ChatInputPanelNode?, currentSecondaryPanel: ChatInputPanelNode?, textInputPanelNode: ChatTextInputPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> (primary: ChatInputPanelNode?, secondary: ChatInputPanelNode?) {
if let renderedPeer = chatPresentationInterfaceState.renderedPeer, renderedPeer.peer?.restrictionText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) != nil {
return (nil, nil)

View File

@ -294,6 +294,7 @@ class ChatSearchResultsControllerNode: ViewControllerTracingNode, ASScrollViewDe
}, dismissNotice: { _ in
}, editPeer: { _ in
}, openWebApp: { _ in
}, openPhotoSetup: {
})
interaction.searchTextHighightState = searchQuery
self.interaction = interaction

View File

@ -179,6 +179,8 @@ private struct CommandChatInputContextPanelEntry: Comparable, Identifiable {
editPeer: { _ in
},
openWebApp: { _ in
},
openPhotoSetup: {
}
)

View File

@ -180,6 +180,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
}, openAgeRestrictedMessageMedia: { _, _ in
}, playMessageEffect: { _ in
}, editMessageFactCheck: { _ in
}, sendGift: { _ in
}, requestMessageUpdate: { _, _ in
}, cancelInteractiveKeyboardGestures: {
}, dismissTextInput: {

View File

@ -1796,6 +1796,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}, openAgeRestrictedMessageMedia: { _, _ in
}, playMessageEffect: { _ in
}, editMessageFactCheck: { _ in
}, sendGift: { _ in
}, requestMessageUpdate: { _, _ in
}, cancelInteractiveKeyboardGestures: {
}, dismissTextInput: {

View File

@ -744,6 +744,10 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
public func openBirthdaySetup() {
self.accountSettingsController?.openBirthdaySetup()
}
public func openPhotoSetup() {
self.accountSettingsController?.openAvatarSetup()
}
}
//Xcode 16