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

View File

@ -959,6 +959,7 @@ public protocol PeerInfoScreen: ViewController {
func toggleStorySelection(ids: [Int32], isSelected: Bool) func toggleStorySelection(ids: [Int32], isSelected: Bool)
func togglePaneIsReordering(isReordering: Bool) func togglePaneIsReordering(isReordering: Bool)
func cancelItemSelection() func cancelItemSelection()
func openAvatarSetup()
} }
public extension Peer { 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 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 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 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 { public func avatarPlaceholderFont(size: CGFloat) -> UIFont {
return Font.with(size: size, design: .round, weight: .bold) return Font.with(size: size, design: .round, weight: .bold)
@ -86,7 +87,7 @@ public func calculateAvatarColors(context: AccountContext?, explicitColorIndex:
} }
let colors: [UIColor] let colors: [UIColor]
if icon != .none { if icon != .none && icon != .cameraIcon {
if case .deletedIcon = icon { if case .deletedIcon = icon {
colors = AvatarNode.grayscaleColors colors = AvatarNode.grayscaleColors
} else if case .phoneIcon = icon { } else if case .phoneIcon = icon {
@ -196,6 +197,7 @@ public enum AvatarNodeIcon: Equatable {
case deletedIcon case deletedIcon
case phoneIcon case phoneIcon
case repostIcon case repostIcon
case cameraIcon
} }
public enum AvatarNodeImageOverride: Equatable { public enum AvatarNodeImageOverride: Equatable {
@ -210,6 +212,7 @@ public enum AvatarNodeImageOverride: Equatable {
case deletedIcon case deletedIcon
case phoneIcon case phoneIcon
case repostIcon case repostIcon
case cameraIcon
} }
public enum AvatarNodeColorOverride { public enum AvatarNodeColorOverride {
@ -540,6 +543,9 @@ public final class AvatarNode: ASDisplayNode {
case .phoneIcon: case .phoneIcon:
representation = nil representation = nil
icon = .phoneIcon icon = .phoneIcon
case .cameraIcon:
representation = nil
icon = .cameraIcon
} }
} else if peer?.restrictionText(platform: "ios", contentSettings: contentSettings) == nil { } else if peer?.restrictionText(platform: "ios", contentSettings: contentSettings) == nil {
representation = peer?.smallProfileImage representation = peer?.smallProfileImage
@ -716,6 +722,9 @@ public final class AvatarNode: ASDisplayNode {
case .phoneIcon: case .phoneIcon:
representation = nil representation = nil
icon = .phoneIcon icon = .phoneIcon
case .cameraIcon:
representation = nil
icon = .cameraIcon
} }
} else if peer?.restrictionText(platform: "ios", contentSettings: genericContext.currentContentSettings.with { $0 }) == nil { } else if peer?.restrictionText(platform: "ios", contentSettings: genericContext.currentContentSettings.with { $0 }) == nil {
representation = peer?.smallProfileImage representation = peer?.smallProfileImage
@ -959,6 +968,15 @@ public final class AvatarNode: ASDisplayNode {
if let myNotesIcon = myNotesIcon { 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)) 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 { } 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.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.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 { if blurred {
context.setBlendMode(.normal) context.setBlendMode(.normal)
context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.45).cgColor) context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.45).cgColor)

View File

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

View File

@ -214,7 +214,8 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode {
theme: PresentationTheme, theme: PresentationTheme,
mode: HorizontalPeerItemMode, mode: HorizontalPeerItemMode,
strings: PresentationStrings, 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.theme = theme
self.strings = strings self.strings = strings
self.themeAndStringsPromise = Promise((self.theme, self.strings)) self.themeAndStringsPromise = Promise((self.theme, self.strings))
@ -225,6 +226,7 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode {
self.isPeerSelected = isPeerSelected self.isPeerSelected = isPeerSelected
self.listView = ListView() self.listView = ListView()
self.listView.preloadPages = false
self.listView.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0) self.listView.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
self.listView.accessibilityPageScrolledString = { row, count in self.listView.accessibilityPageScrolledString = { row, count in
return strings.VoiceOver_ScrollStatus(row, count).string return strings.VoiceOver_ScrollStatus(row, count).string
@ -340,11 +342,6 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode {
) )
strongSelf.enqueueTransition(transition) strongSelf.enqueueTransition(transition)
if !strongSelf.didSetReady {
strongSelf.ready.set(.single(true))
strongSelf.didSetReady = true
}
} }
})) }))
if case .actionSheet = mode { if case .actionSheet = mode {
@ -371,7 +368,20 @@ public final class ChatListSearchRecentPeersNode: ASDisplayNode {
} else if transition.animated { } else if transition.animated {
options.insert(.AnimateInsertion) 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 self.chatListDisplayNode.mainContainerNode.openPremiumManagement = { [weak self] in
guard let self else { guard let self else {
return return

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,6 +13,7 @@ import AccountContext
import MergedAvatarsNode import MergedAvatarsNode
import TextNodeWithEntities import TextNodeWithEntities
import TextFormat import TextFormat
import AvatarNode
class ChatListNoticeItem: ListViewItem { class ChatListNoticeItem: ListViewItem {
enum Action { enum Action {
@ -94,6 +95,7 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode {
private let arrowNode: ASImageNode private let arrowNode: ASImageNode
private let separatorNode: ASDisplayNode private let separatorNode: ASDisplayNode
private var avatarNode: AvatarNode?
private var avatarsNode: MergedAvatarsNode? private var avatarsNode: MergedAvatarsNode?
private var closeButton: HighlightableButtonNode? private var closeButton: HighlightableButtonNode?
@ -175,6 +177,7 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode {
let titleString: NSAttributedString let titleString: NSAttributedString
let textString: NSAttributedString let textString: NSAttributedString
var avatarPeer: EnginePeer?
var avatarPeers: [EnginePeer] = [] var avatarPeers: [EnginePeer] = []
var okButtonLayout: (TextNodeLayout, () -> TextNode)? var okButtonLayout: (TextNodeLayout, () -> TextNode)?
@ -285,12 +288,20 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode {
} }
titleString = attributedTitle titleString = attributedTitle
textString = NSAttributedString(string: text, font: smallTextFont, textColor: item.theme.rootController.navigationBar.secondaryTextColor) 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 var leftInset: CGFloat = sideInset
if !avatarPeers.isEmpty { if !avatarPeers.isEmpty {
let avatarsWidth = 30.0 + CGFloat(avatarPeers.count - 1) * 16.0 let avatarsWidth = 30.0 + CGFloat(avatarPeers.count - 1) * 16.0
leftInset += avatarsWidth + 4.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)) 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 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 { 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) 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 { public func canSendMessagesToChat(_ state: ChatPresentationInterfaceState) -> Bool {
if let peer = state.renderedPeer?.peer { if let peer = state.renderedPeer?.peer {
if canSendMessagesToPeer(peer) { let canBypassRestrictions = canBypassRestrictions(chatPresentationInterfaceState: state)
if canSendMessagesToPeer(peer, ignoreDefault: canBypassRestrictions) {
return true return true
} else { } else {
return false return false

View File

@ -68,6 +68,7 @@ public enum ContactsPeerItemPeerMode: Equatable {
case generalSearch(isSavedMessages: Bool) case generalSearch(isSavedMessages: Bool)
case peer case peer
case memberList case memberList
case app(isPopular: Bool)
} }
public enum ContactsPeerItemBadgeType { public enum ContactsPeerItemBadgeType {
@ -722,7 +723,15 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize) let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
let titleBoldFont = Font.medium(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 badgeFont = Font.regular(14.0)
let avatarDiameter = min(40.0, floor(item.presentationData.fontSize.itemListBaseFontSize * 40.0 / 17.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 var verticalInset: CGFloat = statusAttributedString == nil ? 13.0 : 6.0
let verticalInset: CGFloat = statusAttributedString == nil ? 13.0 : 6.0 if case .app = item.peerMode {
verticalInset += 2.0
}
let statusHeightComponent: CGFloat let statusHeightComponent: CGFloat
if statusAttributedString == nil { if statusAttributedString == nil {
@ -1058,7 +1069,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
let titleFrame: CGRect let titleFrame: CGRect
if statusAttributedString != nil { 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 { } else {
titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((nodeLayout.contentSize.height - titleLayout.size.height) / 2.0)), size: titleLayout.size) 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 { } else if peer.isDeleted {
overrideImage = .deletedIcon overrideImage = .deletedIcon
} }
var displayDimensions = CGSize(width: 60.0, height: 60.0)
let clipStyle: AvatarNodeClipStyle 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 clipStyle = .roundedRect
} else { } else {
clipStyle = .round 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): case let .deviceContact(_, contact):
let letters: [String] 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) strongSelf.avatarNode.frame = CGRect(origin: CGPoint(), size: avatarFrame.size)

View File

@ -97,7 +97,11 @@ final class NavigationTransitionCoordinator {
case .Push: case .Push:
self.container.addSubnode(topNode) self.container.addSubnode(topNode)
case .Pop: 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 { if !self.isFlat {

View File

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

View File

@ -231,6 +231,7 @@ private final class TextSizeSelectionControllerNode: ASDisplayNode, ASScrollView
}, dismissNotice: { _ in }, dismissNotice: { _ in
}, editPeer: { _ in }, editPeer: { _ in
}, openWebApp: { _ 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) 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 }, dismissNotice: { _ in
}, editPeer: { _ in }, editPeer: { _ in
}, openWebApp: { _ in }, openWebApp: { _ in
}, openPhotoSetup: {
}) })
func makeChatListItem( func makeChatListItem(

View File

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

View File

@ -62,7 +62,7 @@ public extension Api {
indirect enum Message: TypeConstructorDescription { 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 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 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) { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self { switch self {
@ -115,9 +115,9 @@ public extension Api {
serializeInt32(id, buffer: buffer, boxed: false) serializeInt32(id, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {peerId!.serialize(buffer, true)} if Int(flags) & Int(1 << 0) != 0 {peerId!.serialize(buffer, true)}
break 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 { if boxed {
buffer.appendInt32(721967202) buffer.appendInt32(-741178048)
} }
serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt32(id, 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)} if Int(flags) & Int(1 << 3) != 0 {replyTo!.serialize(buffer, true)}
serializeInt32(date, buffer: buffer, boxed: false) serializeInt32(date, buffer: buffer, boxed: false)
action.serialize(buffer, true) 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)} if Int(flags) & Int(1 << 25) != 0 {serializeInt32(ttlPeriod!, buffer: buffer, boxed: false)}
break 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)]) 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): case .messageEmpty(let flags, let id, let peerId):
return ("messageEmpty", [("flags", flags as Any), ("id", id as Any), ("peerId", peerId as Any)]) 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): 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), ("ttlPeriod", ttlPeriod as Any)]) 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() { if let signature = reader.readInt32() {
_7 = Api.parse(reader, signature: signature) as? Api.MessageAction _7 = Api.parse(reader, signature: signature) as? Api.MessageAction
} }
var _8: Int32? var _8: Api.MessageReactions?
if Int(_1!) & Int(1 << 25) != 0 {_8 = reader.readInt32() } 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 _c1 = _1 != nil
let _c2 = _2 != nil let _c2 = _2 != nil
let _c3 = (Int(_1!) & Int(1 << 8) == 0) || _3 != 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 _c5 = (Int(_1!) & Int(1 << 3) == 0) || _5 != nil
let _c6 = _6 != nil let _c6 = _6 != nil
let _c7 = _7 != nil let _c7 = _7 != nil
let _c8 = (Int(_1!) & Int(1 << 25) == 0) || _8 != nil let _c8 = (Int(_1!) & Int(1 << 20) == 0) || _8 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { let _c9 = (Int(_1!) & Int(1 << 25) == 0) || _9 != nil
return Api.Message.messageService(flags: _1!, id: _2!, fromId: _3, peerId: _4!, replyTo: _5, date: _6!, action: _7!, ttlPeriod: _8) 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 { else {
return nil return nil

View File

@ -135,7 +135,7 @@ func apiMessagePeerId(_ messsage: Api.Message) -> PeerId? {
} else { } else {
return nil return nil
} }
case let .messageService(_, _, _, chatPeerId, _, _, _, _): case let .messageService(_, _, _, chatPeerId, _, _, _, _, _):
return chatPeerId.peerId return chatPeerId.peerId
} }
} }
@ -216,7 +216,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] {
return result return result
case .messageEmpty: case .messageEmpty:
return [] return []
case let .messageService(_, _, fromId, chatPeerId, _, _, action, _): case let .messageService(_, _, fromId, chatPeerId, _, _, action, _, _):
let peerId: PeerId = chatPeerId.peerId let peerId: PeerId = chatPeerId.peerId
var result = [peerId] var result = [peerId]
@ -295,7 +295,7 @@ func apiMessageAssociatedMessageIds(_ message: Api.Message) -> (replyIds: Refere
} }
case .messageEmpty: case .messageEmpty:
break break
case let .messageService(_, id, _, chatPeerId, replyHeader, _, _, _): case let .messageService(_, id, _, chatPeerId, replyHeader, _, _, _, _):
if let replyHeader = replyHeader { if let replyHeader = replyHeader {
switch replyHeader { switch replyHeader {
case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, replyMedia, replyToTopId, quoteText, quoteEntities, quoteOffset): 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) 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: case .messageEmpty:
return nil 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 peerId: PeerId = chatPeerId.peerId
let authorId: PeerId? = fromId?.peerId ?? chatPeerId.peerId let authorId: PeerId? = fromId?.peerId ?? chatPeerId.peerId
@ -1075,6 +1075,10 @@ extension StoreMessage {
attributes.append(ContentRequiresValidationMessageAttribute()) attributes.append(ContentRequiresValidationMessageAttribute())
} }
if let reactions = reactions {
attributes.append(ReactionsMessageAttribute(apiReactions: reactions))
}
var storeFlags = StoreMessageFlags() var storeFlags = StoreMessageFlags()
if (flags & 2) == 0 { if (flags & 2) == 0 {
let _ = storeFlags.insert(.Incoming) let _ = storeFlags.insert(.Incoming)
@ -1121,6 +1125,10 @@ extension StoreMessage {
storeFlags.insert(.IsForumTopic) 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) 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 { public extension TelegramChannel {
func hasPermission(_ permission: TelegramChannelPermission) -> Bool { func hasPermission(_ permission: TelegramChannelPermission, ignoreDefault: Bool = false) -> Bool {
if self.flags.contains(.isCreator) { if self.flags.contains(.isCreator) {
if case .canBeAnonymous = permission { if case .canBeAnonymous = permission {
if let adminRights = self.adminRights { if let adminRights = self.adminRights {
@ -50,7 +50,7 @@ public extension TelegramChannel {
if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banSendText) { if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banSendText) {
return false return false
} }
if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banSendText) { if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banSendText) && !ignoreDefault {
return false return false
} }
return true return true
@ -69,7 +69,7 @@ public extension TelegramChannel {
if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banSendPhotos) { if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banSendPhotos) {
return false return false
} }
if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banSendPhotos) { if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banSendPhotos) && !ignoreDefault {
return false return false
} }
return true return true
@ -88,7 +88,7 @@ public extension TelegramChannel {
if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banSendVideos) { if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banSendVideos) {
return false return false
} }
if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banSendVideos) { if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banSendVideos) && !ignoreDefault {
return false return false
} }
return true return true
@ -121,7 +121,7 @@ public extension TelegramChannel {
if let bannedRights = self.bannedRights, bannedRights.flags.intersection(flags) == flags { if let bannedRights = self.bannedRights, bannedRights.flags.intersection(flags) == flags {
return false 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 false
} }
return true return true

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@ import Postbox
private final class LinkHelperClass: NSObject { 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" { if let peer = peer as? TelegramUser, peer.addressName == "replies" {
return false return false
} else if peer is TelegramUser || peer is TelegramGroup { } 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 { } else if let peer = peer as? TelegramSecretChat {
return peer.embeddedState == .active return peer.embeddedState == .active
} else if let peer = peer as? TelegramChannel { } else if let peer = peer as? TelegramChannel {
return peer.hasPermission(.sendSomething) return peer.hasPermission(.sendSomething, ignoreDefault: ignoreDefault)
} else { } else {
return false return false
} }

View File

@ -671,6 +671,8 @@ public final class ChatInlineSearchResultsListComponent: Component {
editPeer: { _ in editPeer: { _ in
}, },
openWebApp: { _ in openWebApp: { _ in
},
openPhotoSetup: {
} }
) )
self.chatListNodeInteraction = chatListNodeInteraction 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) var backgroundSize = CGSize(width: labelLayout.size.width + 8.0 + 8.0, height: labelLayout.size.height + 4.0)
if let _ = image { 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.width, { boundingWidth in
return (backgroundSize, { [weak self] animation, synchronousLoads, _ 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 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 { if let image = image {
let imageNode: TransformImageNode let imageNode: TransformImageNode
if let current = strongSelf.imageNode { if let current = strongSelf.imageNode {
@ -319,7 +320,7 @@ public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
attemptSynchronous: synchronousLoads 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 { if story != nil {
let leadingIconView: UIImageView let leadingIconView: UIImageView

View File

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

View File

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

View File

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

View File

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

View File

@ -30,6 +30,7 @@ public final class MessageReactionButtonsNode: ASDisplayNode {
public enum DisplayAlignment { public enum DisplayAlignment {
case left case left
case right case right
case center
} }
private var bubbleBackgroundNode: WallpaperBubbleBackgroundNode? private var bubbleBackgroundNode: WallpaperBubbleBackgroundNode?
@ -224,27 +225,29 @@ public final class MessageReactionButtonsNode: ASDisplayNode {
constrainedWidth: constrainedWidth constrainedWidth: constrainedWidth
) )
let itemSpacing: CGFloat = 6.0
var reactionButtonsSize = CGSize() var reactionButtonsSize = CGSize()
var currentRowWidth: CGFloat = 0.0 var currentRowWidth: CGFloat = 0.0
for item in reactionButtonsResult.items { for item in reactionButtonsResult.items {
if currentRowWidth + item.size.width > constrainedWidth { if currentRowWidth + item.size.width > constrainedWidth {
reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth) reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth)
if !reactionButtonsSize.height.isZero { if !reactionButtonsSize.height.isZero {
reactionButtonsSize.height += 6.0 reactionButtonsSize.height += itemSpacing
} }
reactionButtonsSize.height += item.size.height reactionButtonsSize.height += item.size.height
currentRowWidth = 0.0 currentRowWidth = 0.0
} }
if !currentRowWidth.isZero { if !currentRowWidth.isZero {
currentRowWidth += 6.0 currentRowWidth += itemSpacing
} }
currentRowWidth += item.size.width currentRowWidth += item.size.width
} }
if !currentRowWidth.isZero && !reactionButtonsResult.items.isEmpty { if !currentRowWidth.isZero && !reactionButtonsResult.items.isEmpty {
reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth) reactionButtonsSize.width = max(reactionButtonsSize.width, currentRowWidth)
if !reactionButtonsSize.height.isZero { if !reactionButtonsSize.height.isZero {
reactionButtonsSize.height += 6.0 reactionButtonsSize.height += itemSpacing
} }
reactionButtonsSize.height += reactionButtonsResult.items[0].size.height reactionButtonsSize.height += reactionButtonsResult.items[0].size.height
} }
@ -296,11 +299,14 @@ public final class MessageReactionButtonsNode: ASDisplayNode {
} }
var reactionButtonPosition: CGPoint var reactionButtonPosition: CGPoint
switch alignment { switch alignment {
case .left: case .left:
reactionButtonPosition = CGPoint(x: -1.0, y: topInset) reactionButtonPosition = CGPoint(x: -1.0, y: topInset)
case .right: case .right:
reactionButtonPosition = CGPoint(x: size.width + 1.0, y: topInset) reactionButtonPosition = CGPoint(x: size.width + 1.0, y: topInset)
case .center:
reactionButtonPosition = CGPoint(x: 0.0, y: topInset)
} }
let reactionButtons = reactionButtonsResult.apply( let reactionButtons = reactionButtonsResult.apply(
@ -312,32 +318,8 @@ public final class MessageReactionButtonsNode: ASDisplayNode {
) )
var validIds = Set<MessageReaction.Reaction>() 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 itemMaskFrame = itemFrame.offsetBy(dx: backgroundInsets, dy: backgroundInsets)
let itemMaskView: UIView let itemMaskView: UIView
@ -397,6 +379,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] = [] var removeMaskIds: [MessageReaction.Reaction] = []
for (id, view) in strongSelf.backgroundMaskButtons { for (id, view) in strongSelf.backgroundMaskButtons {
if !validIds.contains(id) { if !validIds.contains(id) {
@ -629,6 +682,7 @@ public final class ChatMessageReactionButtonsNode: ASDisplayNode {
public let accountPeer: EnginePeer? public let accountPeer: EnginePeer?
public let isIncoming: Bool public let isIncoming: Bool
public let constrainedWidth: CGFloat public let constrainedWidth: CGFloat
public let centerAligned: Bool
public init( public init(
context: AccountContext, context: AccountContext,
@ -641,7 +695,8 @@ public final class ChatMessageReactionButtonsNode: ASDisplayNode {
associatedData: ChatMessageItemAssociatedData, associatedData: ChatMessageItemAssociatedData,
accountPeer: EnginePeer?, accountPeer: EnginePeer?,
isIncoming: Bool, isIncoming: Bool,
constrainedWidth: CGFloat constrainedWidth: CGFloat,
centerAligned: Bool
) { ) {
self.context = context self.context = context
self.presentationData = presentationData self.presentationData = presentationData
@ -654,6 +709,7 @@ public final class ChatMessageReactionButtonsNode: ASDisplayNode {
self.accountPeer = accountPeer self.accountPeer = accountPeer
self.isIncoming = isIncoming self.isIncoming = isIncoming
self.constrainedWidth = constrainedWidth self.constrainedWidth = constrainedWidth
self.centerAligned = centerAligned
} }
} }
@ -682,6 +738,13 @@ public final class ChatMessageReactionButtonsNode: ASDisplayNode {
return { arguments in return { arguments in
let node = maybeNode ?? ChatMessageReactionButtonsNode() let node = maybeNode ?? ChatMessageReactionButtonsNode()
let alignment: MessageReactionButtonsNode.DisplayAlignment
if arguments.centerAligned {
alignment = .center
} else {
alignment = arguments.isIncoming ? .left : .right
}
let buttonsUpdate = node.buttonsNode.prepareUpdate( let buttonsUpdate = node.buttonsNode.prepareUpdate(
context: arguments.context, context: arguments.context,
presentationData: arguments.presentationData, presentationData: arguments.presentationData,
@ -692,7 +755,7 @@ public final class ChatMessageReactionButtonsNode: ASDisplayNode {
accountPeer: arguments.accountPeer, accountPeer: arguments.accountPeer,
message: arguments.message, message: arguments.message,
associatedData: arguments.associatedData, associatedData: arguments.associatedData,
alignment: arguments.isIncoming ? .left : .right, alignment: alignment,
constrainedWidth: arguments.constrainedWidth, constrainedWidth: arguments.constrainedWidth,
type: .freeform type: .freeform
) )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3621,6 +3621,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}, openAgeRestrictedMessageMedia: { _, _ in }, openAgeRestrictedMessageMedia: { _, _ in
}, playMessageEffect: { _ in }, playMessageEffect: { _ in
}, editMessageFactCheck: { _ in }, editMessageFactCheck: { _ in
}, sendGift: { _ in
}, requestMessageUpdate: { _, _ in }, requestMessageUpdate: { _, _ in
}, cancelInteractiveKeyboardGestures: { }, cancelInteractiveKeyboardGestures: {
}, dismissTextInput: { }, dismissTextInput: {
@ -9825,7 +9826,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
legacyController.bind(controller: navigationController) legacyController.bind(controller: navigationController)
strongSelf.view.endEditing(true) 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 var hasPhotos = false
if !peer.profileImageRepresentations.isEmpty { 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))! 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) mixin.stickersContext = LegacyPaintStickersContext(context: strongSelf.context)
let _ = strongSelf.currentAvatarMixin.swap(mixin) let _ = strongSelf.currentAvatarMixin.swap(mixin)
mixin.requestSearchController = { [weak self] assetsController in mixin.requestSearchController = { [weak self, weak parentController] assetsController in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
@ -9885,14 +9889,14 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
self?.updateProfilePhoto(result, mode: mode) self?.updateProfilePhoto(result, mode: mode)
})) }))
controller.navigationPresentation = .modal controller.navigationPresentation = .modal
(strongSelf.controller?.navigationController?.topViewController as? ViewController)?.push(controller) parentController?.push(controller)
if fromGallery { if fromGallery {
completion(nil) completion(nil)
} }
} }
var isFromEditor = false 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 { guard let strongSelf = self, let imageCompletion, let videoCompletion else {
return return
} }
@ -9913,27 +9917,33 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
let controller = AvatarEditorScreen(context: strongSelf.context, inputData: keyboardInputData.get(), peerType: peerType, markup: emojiMarkup) let controller = AvatarEditorScreen(context: strongSelf.context, inputData: keyboardInputData.get(), peerType: peerType, markup: emojiMarkup)
controller.imageCompletion = imageCompletion controller.imageCompletion = imageCompletion
controller.videoCompletion = videoCompletion controller.videoCompletion = videoCompletion
(strongSelf.controller?.navigationController?.topViewController as? ViewController)?.push(controller) parentController?.push(controller)
isFromEditor = true isFromEditor = true
Queue.mainQueue().after(1.0) {
if let rootController = strongSelf.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
rootController.openSettings()
}
}
} }
if let confirmationTextPhoto, let confirmationAction { 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 { if let strongSelf = self, let image {
let controller = photoUpdateConfirmationController(context: strongSelf.context, peer: peer, image: image, text: confirmationTextPhoto, doneTitle: confirmationAction, commit: { let controller = photoUpdateConfirmationController(context: strongSelf.context, peer: peer, image: image, text: confirmationTextPhoto, doneTitle: confirmationAction, commit: {
commit?() commit?()
}) })
(strongSelf.controller?.navigationController?.topViewController as? ViewController)?.presentInGlobalOverlay(controller) parentController?.presentInGlobalOverlay(controller)
} }
} }
} }
if let confirmationTextVideo, let confirmationAction { 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 { if let strongSelf = self, let image {
let controller = photoUpdateConfirmationController(context: strongSelf.context, peer: peer, image: image, text: confirmationTextVideo, doneTitle: confirmationAction, isDark: !isFromEditor, commit: { let controller = photoUpdateConfirmationController(context: strongSelf.context, peer: peer, image: image, text: confirmationTextVideo, doneTitle: confirmationAction, isDark: !isFromEditor, commit: {
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) { public func updateProfilePhoto(_ image: UIImage, mode: PeerInfoAvatarEditingMode) {
if !self.isNodeLoaded { if !self.isNodeLoaded {
self.loadDisplayNode() self.loadDisplayNode()

View File

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

View File

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

View File

@ -874,6 +874,7 @@ final class ThemeAccentColorControllerNode: ASDisplayNode, ASScrollViewDelegate
}, dismissNotice: { _ in }, dismissNotice: { _ in
}, editPeer: { _ in }, editPeer: { _ in
}, openWebApp: { _ 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) 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 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() strongSelf.interfaceInteraction?.openBoostToUnrestrict()
return false 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 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() strongSelf.interfaceInteraction?.openBoostToUnrestrict()
return false 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 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() strongSelf.interfaceInteraction?.openBoostToUnrestrict()
return false return false
} }
@ -4554,6 +4554,20 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return return
} }
self.openEditMessageFactCheck(messageId: messageId) 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 }, requestMessageUpdate: { [weak self] id, scroll in
if let self { if let self {
self.chatDisplayNode.historyNode.requestMessageUpdate(id, andScrollToItem: scroll) self.chatDisplayNode.historyNode.requestMessageUpdate(id, andScrollToItem: scroll)

View File

@ -339,7 +339,8 @@ func canReplyInChat(_ chatPresentationInterfaceState: ChatPresentationInterfaceS
case .peer: case .peer:
if let channel = peer as? TelegramChannel { if let channel = peer as? TelegramChannel {
if case .member = channel.participationStatus { 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 { if case .broadcast = channel.info {
canReply = true 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 var isReplyThreadHead = false
if case let .replyThread(replyThreadMessage) = chatPresentationInterfaceState.chatLocation { if case let .replyThread(replyThreadMessage) = chatPresentationInterfaceState.chatLocation {
isReplyThreadHead = messages[0].id == replyThreadMessage.effectiveTopId isReplyThreadHead = messages[0].id == replyThreadMessage.effectiveTopId
@ -2220,8 +2238,15 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer
} }
break break
} }
} else if let action = media as? TelegramMediaAction, case .phoneCall = action.action { } else if let action = media as? TelegramMediaAction {
optionsMap[id]!.insert(.rateCall) switch action.action {
case .phoneCall:
optionsMap[id]!.insert(.rateCall)
case .starGift:
optionsMap[id]!.insert(.sendGift)
default:
break
}
} else if let story = media as? TelegramMediaStory { } else if let story = media as? TelegramMediaStory {
if let story = message.associatedStories[story.storyId], story.data.isEmpty { if let story = message.associatedStories[story.storyId], story.data.isEmpty {
isShareProtected = true isShareProtected = true

View File

@ -9,16 +9,6 @@ import ChatBotStartInputPanelNode
import ChatChannelSubscriberInputPanelNode import ChatChannelSubscriberInputPanelNode
import ChatMessageSelectionInputPanelNode 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?) { 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 { if let renderedPeer = chatPresentationInterfaceState.renderedPeer, renderedPeer.peer?.restrictionText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) != nil {
return (nil, nil) return (nil, nil)

View File

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

View File

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

View File

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

View File

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

View File

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