mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
63714bec47
@ -805,15 +805,18 @@ public struct StoryCameraTransitionOut {
|
||||
public weak var destinationView: UIView?
|
||||
public let destinationRect: CGRect
|
||||
public let destinationCornerRadius: CGFloat
|
||||
public let completion: (() -> Void)?
|
||||
|
||||
public init(
|
||||
destinationView: UIView,
|
||||
destinationRect: CGRect,
|
||||
destinationCornerRadius: CGFloat
|
||||
destinationCornerRadius: CGFloat,
|
||||
completion: (() -> Void)? = nil
|
||||
) {
|
||||
self.destinationView = destinationView
|
||||
self.destinationRect = destinationRect
|
||||
self.destinationCornerRadius = destinationCornerRadius
|
||||
self.completion = completion
|
||||
}
|
||||
}
|
||||
|
||||
@ -909,6 +912,12 @@ public struct ChatControllerParams {
|
||||
}
|
||||
}
|
||||
|
||||
public enum ChatOpenWebViewSource: Equatable {
|
||||
case generic
|
||||
case menu
|
||||
case inline(bot: EnginePeer)
|
||||
}
|
||||
|
||||
public protocol SharedAccountContext: AnyObject {
|
||||
var sharedContainerPath: String { get }
|
||||
var basePath: String { get }
|
||||
@ -1077,6 +1086,7 @@ public protocol SharedAccountContext: AnyObject {
|
||||
func makeStarsAmountScreen(context: AccountContext, initialValue: Int64?, completion: @escaping (Int64) -> Void) -> ViewController
|
||||
func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController
|
||||
func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController
|
||||
func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool)
|
||||
|
||||
func makeDebugSettingsController(context: AccountContext?) -> ViewController?
|
||||
|
||||
|
@ -951,6 +951,7 @@ public protocol PeerInfoScreen: ViewController {
|
||||
|
||||
func openBirthdaySetup()
|
||||
func toggleStorySelection(ids: [Int32], isSelected: Bool)
|
||||
func togglePaneIsReordering(isReordering: Bool)
|
||||
func cancelItemSelection()
|
||||
}
|
||||
|
||||
|
@ -141,387 +141,398 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
|
||||
})))
|
||||
items.append(.separator)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let isSavedMessages = peerId == context.account.peerId
|
||||
|
||||
if !isSavedMessages, case let .user(peer) = peer, !peer.flags.contains(.isSupport), peer.botInfo == nil && !peer.isDeleted {
|
||||
if !isContact {
|
||||
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_AddToContacts, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
context.sharedContext.openAddPersonContact(context: context, peerId: peerId, pushController: { controller in
|
||||
if let navigationController = chatListController?.navigationController as? NavigationController {
|
||||
navigationController.pushViewController(controller)
|
||||
}
|
||||
}, present: { c, a in
|
||||
if let chatListController = chatListController {
|
||||
chatListController.present(c, in: .window(.root), with: a)
|
||||
}
|
||||
case .recentApps:
|
||||
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_RemoveFromRecents, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Clear"), color: theme.contextMenu.destructiveColor) }, action: { _, f in
|
||||
let _ = (context.engine.peers.removeRecentlyUsedApp(peerId: peerId)
|
||||
|> deliverOnMainQueue).startStandalone(completed: {
|
||||
f(.default)
|
||||
})
|
||||
f(.default)
|
||||
})))
|
||||
items.append(.separator)
|
||||
}
|
||||
}
|
||||
|
||||
var isMuted = false
|
||||
if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) {
|
||||
isMuted = true
|
||||
} else if case .default = notificationSettings.muteState {
|
||||
if case .user = peer {
|
||||
isMuted = !globalNotificationSettings.privateChats.enabled
|
||||
} else if case .legacyGroup = peer {
|
||||
isMuted = !globalNotificationSettings.groupChats.enabled
|
||||
} else if case let .channel(channel) = peer {
|
||||
switch channel.info {
|
||||
case .group:
|
||||
isMuted = !globalNotificationSettings.groupChats.enabled
|
||||
case .broadcast:
|
||||
isMuted = !globalNotificationSettings.channels.enabled
|
||||
if case .search(.recentApps) = source {
|
||||
} else {
|
||||
let isSavedMessages = peerId == context.account.peerId
|
||||
|
||||
if !isSavedMessages, case let .user(peer) = peer, !peer.flags.contains(.isSupport), peer.botInfo == nil && !peer.isDeleted {
|
||||
if !isContact {
|
||||
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_AddToContacts, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/AddUser"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
context.sharedContext.openAddPersonContact(context: context, peerId: peerId, pushController: { controller in
|
||||
if let navigationController = chatListController?.navigationController as? NavigationController {
|
||||
navigationController.pushViewController(controller)
|
||||
}
|
||||
}, present: { c, a in
|
||||
if let chatListController = chatListController {
|
||||
chatListController.present(c, in: .window(.root), with: a)
|
||||
}
|
||||
})
|
||||
f(.default)
|
||||
})))
|
||||
items.append(.separator)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isUnread = false
|
||||
if readCounters.isUnread {
|
||||
isUnread = true
|
||||
}
|
||||
|
||||
var isForum = false
|
||||
if case let .channel(channel) = peer, channel.flags.contains(.isForum) {
|
||||
isForum = true
|
||||
}
|
||||
|
||||
var hasRemoveFromFolder = false
|
||||
if case let .chatList(currentFilter) = source {
|
||||
if let currentFilter = currentFilter, case let .filter(id, title, emoticon, data) = currentFilter {
|
||||
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_RemoveFromFolder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/RemoveFromFolder"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
|
||||
let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in
|
||||
var filters = filters
|
||||
for i in 0 ..< filters.count {
|
||||
if filters[i].id == currentFilter.id {
|
||||
var updatedData = data
|
||||
let _ = updatedData.addExcludePeer(peerId: peer.id)
|
||||
filters[i] = .filter(id: id, title: title, emoticon: emoticon, data: updatedData)
|
||||
break
|
||||
|
||||
var isMuted = false
|
||||
if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) {
|
||||
isMuted = true
|
||||
} else if case .default = notificationSettings.muteState {
|
||||
if case .user = peer {
|
||||
isMuted = !globalNotificationSettings.privateChats.enabled
|
||||
} else if case .legacyGroup = peer {
|
||||
isMuted = !globalNotificationSettings.groupChats.enabled
|
||||
} else if case let .channel(channel) = peer {
|
||||
switch channel.info {
|
||||
case .group:
|
||||
isMuted = !globalNotificationSettings.groupChats.enabled
|
||||
case .broadcast:
|
||||
isMuted = !globalNotificationSettings.channels.enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isUnread = false
|
||||
if readCounters.isUnread {
|
||||
isUnread = true
|
||||
}
|
||||
|
||||
var isForum = false
|
||||
if case let .channel(channel) = peer, channel.flags.contains(.isForum) {
|
||||
isForum = true
|
||||
}
|
||||
|
||||
var hasRemoveFromFolder = false
|
||||
if case let .chatList(currentFilter) = source {
|
||||
if let currentFilter = currentFilter, case let .filter(id, title, emoticon, data) = currentFilter {
|
||||
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_RemoveFromFolder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/RemoveFromFolder"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
|
||||
let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in
|
||||
var filters = filters
|
||||
for i in 0 ..< filters.count {
|
||||
if filters[i].id == currentFilter.id {
|
||||
var updatedData = data
|
||||
let _ = updatedData.addExcludePeer(peerId: peer.id)
|
||||
filters[i] = .filter(id: id, title: title, emoticon: emoticon, data: updatedData)
|
||||
break
|
||||
}
|
||||
}
|
||||
return filters
|
||||
}
|
||||
|> deliverOnMainQueue).startStandalone(completed: {
|
||||
c?.dismiss(completion: {
|
||||
chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatRemovedFromFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in
|
||||
return false
|
||||
}), in: .current)
|
||||
})
|
||||
})
|
||||
})))
|
||||
hasRemoveFromFolder = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasRemoveFromFolder && peerGroup != nil {
|
||||
var hasFolders = false
|
||||
|
||||
for case let .filter(_, _, _, data) in filters {
|
||||
let predicate = chatListFilterPredicate(filter: data, accountPeerId: context.account.peerId)
|
||||
if predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: false) {
|
||||
continue
|
||||
}
|
||||
|
||||
var data = data
|
||||
if data.addIncludePeer(peerId: peer.id) {
|
||||
hasFolders = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hasFolders {
|
||||
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_AddToFolder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Folder"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
|
||||
var updatedItems: [ContextMenuItem] = []
|
||||
|
||||
for filter in filters {
|
||||
if case let .filter(_, title, _, data) = filter {
|
||||
let predicate = chatListFilterPredicate(filter: data, accountPeerId: context.account.peerId)
|
||||
if predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: false) {
|
||||
continue
|
||||
}
|
||||
|
||||
var data = data
|
||||
if !data.addIncludePeer(peerId: peer.id) {
|
||||
continue
|
||||
}
|
||||
|
||||
let filterType = chatListFilterType(data)
|
||||
updatedItems.append(.action(ContextMenuActionItem(text: title, icon: { theme in
|
||||
let imageName: String
|
||||
switch filterType {
|
||||
case .generic:
|
||||
imageName = "Chat/Context Menu/List"
|
||||
case .unmuted:
|
||||
imageName = "Chat/Context Menu/Unmute"
|
||||
case .unread:
|
||||
imageName = "Chat/Context Menu/MarkAsUnread"
|
||||
case .channels:
|
||||
imageName = "Chat/Context Menu/Channels"
|
||||
case .groups:
|
||||
imageName = "Chat/Context Menu/Groups"
|
||||
case .bots:
|
||||
imageName = "Chat/Context Menu/Bots"
|
||||
case .contacts:
|
||||
imageName = "Chat/Context Menu/User"
|
||||
case .nonContacts:
|
||||
imageName = "Chat/Context Menu/UnknownUser"
|
||||
}
|
||||
return generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.contextMenu.primaryColor)
|
||||
}, action: { c, f in
|
||||
c?.dismiss(completion: {
|
||||
let isPremium = limitsData.0?.isPremium ?? false
|
||||
let (_, limits, premiumLimits) = limitsData
|
||||
|
||||
let limit = limits.maxFolderChatsCount
|
||||
let premiumLimit = premiumLimits.maxFolderChatsCount
|
||||
|
||||
let count = data.includePeers.peers.count - 1
|
||||
if count >= premiumLimit {
|
||||
let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: {
|
||||
return true
|
||||
})
|
||||
chatListController?.push(controller)
|
||||
return
|
||||
} else if count >= limit && !isPremium {
|
||||
var replaceImpl: ((ViewController) -> Void)?
|
||||
let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: {
|
||||
let controller = PremiumIntroScreen(context: context, source: .chatsPerFolder)
|
||||
replaceImpl?(controller)
|
||||
return true
|
||||
})
|
||||
replaceImpl = { [weak controller] c in
|
||||
controller?.replace(with: c)
|
||||
}
|
||||
chatListController?.push(controller)
|
||||
return
|
||||
}
|
||||
|
||||
let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in
|
||||
var filters = filters
|
||||
for i in 0 ..< filters.count {
|
||||
if filters[i].id == filter.id {
|
||||
if case let .filter(id, title, emoticon, data) = filter {
|
||||
var updatedData = data
|
||||
let _ = updatedData.addIncludePeer(peerId: peer.id)
|
||||
filters[i] = .filter(id: id, title: title, emoticon: emoticon, data: updatedData)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return filters
|
||||
}).startStandalone()
|
||||
|
||||
chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatAddedToFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in
|
||||
return false
|
||||
}), in: .current)
|
||||
})
|
||||
})))
|
||||
}
|
||||
}
|
||||
return filters
|
||||
}
|
||||
|> deliverOnMainQueue).startStandalone(completed: {
|
||||
c?.dismiss(completion: {
|
||||
chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatRemovedFromFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in
|
||||
return false
|
||||
}), in: .current)
|
||||
})
|
||||
})
|
||||
|
||||
updatedItems.append(.separator)
|
||||
updatedItems.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Back, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor)
|
||||
}, iconPosition: .left, action: { c, _ in
|
||||
c?.setItems(chatContextMenuItems(context: context, peerId: peerId, promoInfo: promoInfo, source: source, chatListController: chatListController, joined: joined) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true)
|
||||
})))
|
||||
|
||||
c?.setItems(.single(ContextController.Items(content: .list(updatedItems))), minHeight: nil, animated: true)
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
if isUnread {
|
||||
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_MarkAsRead, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MarkAsRead"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
let _ = context.engine.messages.togglePeersUnreadMarkInteractively(peerIds: [peerId], setToValue: nil).startStandalone()
|
||||
f(.default)
|
||||
})))
|
||||
} else if !isForum {
|
||||
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_MarkAsUnread, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MarkAsUnread"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
let _ = context.engine.messages.togglePeersUnreadMarkInteractively(peerIds: [peerId], setToValue: nil).startStandalone()
|
||||
f(.default)
|
||||
})))
|
||||
hasRemoveFromFolder = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasRemoveFromFolder && peerGroup != nil {
|
||||
var hasFolders = false
|
||||
|
||||
for case let .filter(_, _, _, data) in filters {
|
||||
let predicate = chatListFilterPredicate(filter: data, accountPeerId: context.account.peerId)
|
||||
if predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: false) {
|
||||
continue
|
||||
}
|
||||
|
||||
var data = data
|
||||
if data.addIncludePeer(peerId: peer.id) {
|
||||
hasFolders = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hasFolders {
|
||||
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_AddToFolder, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Folder"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
|
||||
var updatedItems: [ContextMenuItem] = []
|
||||
|
||||
for filter in filters {
|
||||
if case let .filter(_, title, _, data) = filter {
|
||||
let predicate = chatListFilterPredicate(filter: data, accountPeerId: context.account.peerId)
|
||||
if predicate.includes(peer: peer._asPeer(), groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: false) {
|
||||
continue
|
||||
|
||||
let archiveEnabled = !isSavedMessages && peerId != PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(777000)) && peerId == context.account.peerId
|
||||
if let group = peerGroup {
|
||||
if archiveEnabled {
|
||||
let isArchived = group == .archive
|
||||
items.append(.action(ContextMenuActionItem(text: isArchived ? strings.ChatList_Context_Unarchive : strings.ChatList_Context_Archive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isArchived ? "Chat/Context Menu/Unarchive" : "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
if isArchived {
|
||||
let _ = (context.engine.peers.updatePeersGroupIdInteractively(peerIds: [peerId], groupId: .root)
|
||||
|> deliverOnMainQueue).startStandalone(completed: {
|
||||
f(.default)
|
||||
})
|
||||
} else {
|
||||
if let chatListController = chatListController {
|
||||
chatListController.archiveChats(peerIds: [peerId])
|
||||
f(.default)
|
||||
} else {
|
||||
let _ = (context.engine.peers.updatePeersGroupIdInteractively(peerIds: [peerId], groupId: .archive)
|
||||
|> deliverOnMainQueue).startStandalone(completed: {
|
||||
f(.default)
|
||||
})
|
||||
}
|
||||
|
||||
var data = data
|
||||
if !data.addIncludePeer(peerId: peer.id) {
|
||||
continue
|
||||
}
|
||||
|
||||
let filterType = chatListFilterType(data)
|
||||
updatedItems.append(.action(ContextMenuActionItem(text: title, icon: { theme in
|
||||
let imageName: String
|
||||
switch filterType {
|
||||
case .generic:
|
||||
imageName = "Chat/Context Menu/List"
|
||||
case .unmuted:
|
||||
imageName = "Chat/Context Menu/Unmute"
|
||||
case .unread:
|
||||
imageName = "Chat/Context Menu/MarkAsUnread"
|
||||
case .channels:
|
||||
imageName = "Chat/Context Menu/Channels"
|
||||
case .groups:
|
||||
imageName = "Chat/Context Menu/Groups"
|
||||
case .bots:
|
||||
imageName = "Chat/Context Menu/Bots"
|
||||
case .contacts:
|
||||
imageName = "Chat/Context Menu/User"
|
||||
case .nonContacts:
|
||||
imageName = "Chat/Context Menu/UnknownUser"
|
||||
}
|
||||
return generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.contextMenu.primaryColor)
|
||||
}, action: { c, f in
|
||||
c?.dismiss(completion: {
|
||||
let isPremium = limitsData.0?.isPremium ?? false
|
||||
let (_, limits, premiumLimits) = limitsData
|
||||
|
||||
let limit = limits.maxFolderChatsCount
|
||||
let premiumLimit = premiumLimits.maxFolderChatsCount
|
||||
|
||||
let count = data.includePeers.peers.count - 1
|
||||
if count >= premiumLimit {
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
if isPinned || chatListFilter == nil || peerId.namespace != Namespaces.Peer.SecretChat {
|
||||
items.append(.action(ContextMenuActionItem(text: isPinned ? strings.ChatList_Context_Unpin : strings.ChatList_Context_Pin, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isPinned ? "Chat/Context Menu/Unpin" : "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { c, f in
|
||||
let _ = (context.engine.peers.toggleItemPinned(location: location, itemId: .peer(peerId))
|
||||
|> deliverOnMainQueue).startStandalone(next: { result in
|
||||
switch result {
|
||||
case .done:
|
||||
f(.default)
|
||||
case let .limitExceeded(count, _):
|
||||
f(.default)
|
||||
|
||||
let isPremium = limitsData.0?.isPremium ?? false
|
||||
if isPremium {
|
||||
if case .filter = location {
|
||||
let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: {
|
||||
return true
|
||||
})
|
||||
chatListController?.push(controller)
|
||||
return
|
||||
} else if count >= limit && !isPremium {
|
||||
var replaceImpl: ((ViewController) -> Void)?
|
||||
let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: {
|
||||
let controller = PremiumIntroScreen(context: context, source: .chatsPerFolder)
|
||||
replaceImpl?(controller)
|
||||
} else {
|
||||
let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: {
|
||||
return true
|
||||
})
|
||||
chatListController?.push(controller)
|
||||
}
|
||||
} else {
|
||||
if case .filter = location {
|
||||
var replaceImpl: ((ViewController) -> Void)?
|
||||
let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: {
|
||||
let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats)
|
||||
replaceImpl?(premiumScreen)
|
||||
return true
|
||||
})
|
||||
chatListController?.push(controller)
|
||||
replaceImpl = { [weak controller] c in
|
||||
controller?.replace(with: c)
|
||||
}
|
||||
} else {
|
||||
var replaceImpl: ((ViewController) -> Void)?
|
||||
let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: {
|
||||
let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats)
|
||||
replaceImpl?(premiumScreen)
|
||||
return true
|
||||
})
|
||||
chatListController?.push(controller)
|
||||
return
|
||||
}
|
||||
|
||||
let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in
|
||||
var filters = filters
|
||||
for i in 0 ..< filters.count {
|
||||
if filters[i].id == filter.id {
|
||||
if case let .filter(id, title, emoticon, data) = filter {
|
||||
var updatedData = data
|
||||
let _ = updatedData.addIncludePeer(peerId: peer.id)
|
||||
filters[i] = .filter(id: id, title: title, emoticon: emoticon, data: updatedData)
|
||||
}
|
||||
break
|
||||
}
|
||||
replaceImpl = { [weak controller] c in
|
||||
controller?.replace(with: c)
|
||||
}
|
||||
return filters
|
||||
}).startStandalone()
|
||||
|
||||
chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatAddedToFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in
|
||||
return false
|
||||
}), in: .current)
|
||||
})
|
||||
})))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})))
|
||||
}
|
||||
|
||||
if !isSavedMessages {
|
||||
var isMuted = false
|
||||
if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) {
|
||||
isMuted = true
|
||||
} else if case .default = notificationSettings.muteState {
|
||||
if case .user = peer {
|
||||
isMuted = !globalNotificationSettings.privateChats.enabled
|
||||
} else if case .legacyGroup = peer {
|
||||
isMuted = !globalNotificationSettings.groupChats.enabled
|
||||
} else if case let .channel(channel) = peer {
|
||||
switch channel.info {
|
||||
case .group:
|
||||
isMuted = !globalNotificationSettings.groupChats.enabled
|
||||
case .broadcast:
|
||||
isMuted = !globalNotificationSettings.channels.enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updatedItems.append(.separator)
|
||||
updatedItems.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Back, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor)
|
||||
}, iconPosition: .left, action: { c, _ in
|
||||
c?.setItems(chatContextMenuItems(context: context, peerId: peerId, promoInfo: promoInfo, source: source, chatListController: chatListController, joined: joined) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil, animated: true)
|
||||
})))
|
||||
|
||||
c?.setItems(.single(ContextController.Items(content: .list(updatedItems))), minHeight: nil, animated: true)
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
if isUnread {
|
||||
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_MarkAsRead, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MarkAsRead"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
let _ = context.engine.messages.togglePeersUnreadMarkInteractively(peerIds: [peerId], setToValue: nil).startStandalone()
|
||||
f(.default)
|
||||
})))
|
||||
} else if !isForum {
|
||||
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_MarkAsUnread, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MarkAsUnread"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
let _ = context.engine.messages.togglePeersUnreadMarkInteractively(peerIds: [peerId], setToValue: nil).startStandalone()
|
||||
f(.default)
|
||||
})))
|
||||
}
|
||||
|
||||
let archiveEnabled = !isSavedMessages && peerId != PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(777000)) && peerId == context.account.peerId
|
||||
if let group = peerGroup {
|
||||
if archiveEnabled {
|
||||
let isArchived = group == .archive
|
||||
items.append(.action(ContextMenuActionItem(text: isArchived ? strings.ChatList_Context_Unarchive : strings.ChatList_Context_Archive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isArchived ? "Chat/Context Menu/Unarchive" : "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
if isArchived {
|
||||
let _ = (context.engine.peers.updatePeersGroupIdInteractively(peerIds: [peerId], groupId: .root)
|
||||
|> deliverOnMainQueue).startStandalone(completed: {
|
||||
items.append(.action(ContextMenuActionItem(text: isMuted ? strings.ChatList_Context_Unmute : strings.ChatList_Context_Mute, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
let _ = (context.engine.peers.togglePeerMuted(peerId: peerId, threadId: nil)
|
||||
|> deliverOnMainQueue).startStandalone(completed: {
|
||||
f(.default)
|
||||
})
|
||||
} else {
|
||||
if let chatListController = chatListController {
|
||||
chatListController.archiveChats(peerIds: [peerId])
|
||||
f(.default)
|
||||
} else {
|
||||
let _ = (context.engine.peers.updatePeersGroupIdInteractively(peerIds: [peerId], groupId: .archive)
|
||||
|> deliverOnMainQueue).startStandalone(completed: {
|
||||
f(.default)
|
||||
})
|
||||
}
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
if isPinned || chatListFilter == nil || peerId.namespace != Namespaces.Peer.SecretChat {
|
||||
items.append(.action(ContextMenuActionItem(text: isPinned ? strings.ChatList_Context_Unpin : strings.ChatList_Context_Pin, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isPinned ? "Chat/Context Menu/Unpin" : "Chat/Context Menu/Pin"), color: theme.contextMenu.primaryColor) }, action: { c, f in
|
||||
let _ = (context.engine.peers.toggleItemPinned(location: location, itemId: .peer(peerId))
|
||||
|> deliverOnMainQueue).startStandalone(next: { result in
|
||||
switch result {
|
||||
case .done:
|
||||
f(.default)
|
||||
case let .limitExceeded(count, _):
|
||||
f(.default)
|
||||
|
||||
let isPremium = limitsData.0?.isPremium ?? false
|
||||
if isPremium {
|
||||
if case .filter = location {
|
||||
let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: {
|
||||
return true
|
||||
})
|
||||
chatListController?.push(controller)
|
||||
} else {
|
||||
let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: {
|
||||
return true
|
||||
})
|
||||
chatListController?.push(controller)
|
||||
}
|
||||
} else {
|
||||
if case .filter = location {
|
||||
var replaceImpl: ((ViewController) -> Void)?
|
||||
let controller = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: Int32(count), action: {
|
||||
let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats)
|
||||
replaceImpl?(premiumScreen)
|
||||
return true
|
||||
})
|
||||
chatListController?.push(controller)
|
||||
replaceImpl = { [weak controller] c in
|
||||
controller?.replace(with: c)
|
||||
}
|
||||
} else {
|
||||
var replaceImpl: ((ViewController) -> Void)?
|
||||
let controller = PremiumLimitScreen(context: context, subject: .pins, count: Int32(count), action: {
|
||||
let premiumScreen = PremiumIntroScreen(context: context, source: .pinnedChats)
|
||||
replaceImpl?(premiumScreen)
|
||||
return true
|
||||
})
|
||||
chatListController?.push(controller)
|
||||
replaceImpl = { [weak controller] c in
|
||||
controller?.replace(with: c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})))
|
||||
}
|
||||
|
||||
if !isSavedMessages {
|
||||
var isMuted = false
|
||||
if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) {
|
||||
isMuted = true
|
||||
} else if case .default = notificationSettings.muteState {
|
||||
if case .user = peer {
|
||||
isMuted = !globalNotificationSettings.privateChats.enabled
|
||||
} else if case .legacyGroup = peer {
|
||||
isMuted = !globalNotificationSettings.groupChats.enabled
|
||||
} else if case let .channel(channel) = peer {
|
||||
switch channel.info {
|
||||
case .group:
|
||||
isMuted = !globalNotificationSettings.groupChats.enabled
|
||||
case .broadcast:
|
||||
isMuted = !globalNotificationSettings.channels.enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
items.append(.action(ContextMenuActionItem(text: isMuted ? strings.ChatList_Context_Unmute : strings.ChatList_Context_Mute, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: isMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
let _ = (context.engine.peers.togglePeerMuted(peerId: peerId, threadId: nil)
|
||||
|> deliverOnMainQueue).startStandalone(completed: {
|
||||
f(.default)
|
||||
})
|
||||
})))
|
||||
}
|
||||
} else {
|
||||
if case .search = source {
|
||||
if case let .channel(peer) = peer {
|
||||
let text: String
|
||||
if case .broadcast = peer.info {
|
||||
text = strings.ChatList_Context_JoinChannel
|
||||
} else {
|
||||
text = strings.ChatList_Context_JoinChat
|
||||
}
|
||||
items.append(.action(ContextMenuActionItem(text: text, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
var createSignal = context.peerChannelMemberCategoriesContextsManager.join(engine: context.engine, peerId: peerId, hash: nil)
|
||||
var cancelImpl: (() -> Void)?
|
||||
let progressSignal = Signal<Never, NoError> { subscriber in
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||
cancelImpl?()
|
||||
}))
|
||||
chatListController?.present(controller, in: .window(.root))
|
||||
return ActionDisposable { [weak controller] in
|
||||
Queue.mainQueue().async() {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|> runOn(Queue.mainQueue())
|
||||
|> delay(0.15, queue: Queue.mainQueue())
|
||||
let progressDisposable = progressSignal.start()
|
||||
|
||||
createSignal = createSignal
|
||||
|> afterDisposed {
|
||||
Queue.mainQueue().async {
|
||||
progressDisposable.dispose()
|
||||
}
|
||||
}
|
||||
let joinChannelDisposable = MetaDisposable()
|
||||
cancelImpl = {
|
||||
joinChannelDisposable.set(nil)
|
||||
}
|
||||
|
||||
joinChannelDisposable.set((createSignal
|
||||
|> deliverOnMainQueue).start(next: { _ in
|
||||
}, error: { _ in
|
||||
if let chatListController = chatListController {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
chatListController.present(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}
|
||||
}, completed: {
|
||||
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|
||||
|> deliverOnMainQueue).startStandalone(next: { peer in
|
||||
guard let peer = peer else {
|
||||
return
|
||||
}
|
||||
if let navigationController = (chatListController?.navigationController as? NavigationController) {
|
||||
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer)))
|
||||
}
|
||||
})
|
||||
}))
|
||||
f(.default)
|
||||
})))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if case .chatList = source, peerGroup != nil {
|
||||
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { _, f in
|
||||
if let chatListController = chatListController {
|
||||
chatListController.deletePeerChat(peerId: peerId, joined: joined)
|
||||
} else {
|
||||
if case .search = source {
|
||||
if case let .channel(peer) = peer {
|
||||
let text: String
|
||||
if case .broadcast = peer.info {
|
||||
text = strings.ChatList_Context_JoinChannel
|
||||
} else {
|
||||
text = strings.ChatList_Context_JoinChat
|
||||
}
|
||||
items.append(.action(ContextMenuActionItem(text: text, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
var createSignal = context.peerChannelMemberCategoriesContextsManager.join(engine: context.engine, peerId: peerId, hash: nil)
|
||||
var cancelImpl: (() -> Void)?
|
||||
let progressSignal = Signal<Never, NoError> { subscriber in
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||
cancelImpl?()
|
||||
}))
|
||||
chatListController?.present(controller, in: .window(.root))
|
||||
return ActionDisposable { [weak controller] in
|
||||
Queue.mainQueue().async() {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|> runOn(Queue.mainQueue())
|
||||
|> delay(0.15, queue: Queue.mainQueue())
|
||||
let progressDisposable = progressSignal.start()
|
||||
|
||||
createSignal = createSignal
|
||||
|> afterDisposed {
|
||||
Queue.mainQueue().async {
|
||||
progressDisposable.dispose()
|
||||
}
|
||||
}
|
||||
let joinChannelDisposable = MetaDisposable()
|
||||
cancelImpl = {
|
||||
joinChannelDisposable.set(nil)
|
||||
}
|
||||
|
||||
joinChannelDisposable.set((createSignal
|
||||
|> deliverOnMainQueue).start(next: { _ in
|
||||
}, error: { _ in
|
||||
if let chatListController = chatListController {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
chatListController.present(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
|
||||
}
|
||||
}, completed: {
|
||||
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|
||||
|> deliverOnMainQueue).startStandalone(next: { peer in
|
||||
guard let peer = peer else {
|
||||
return
|
||||
}
|
||||
if let navigationController = (chatListController?.navigationController as? NavigationController) {
|
||||
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer)))
|
||||
}
|
||||
})
|
||||
}))
|
||||
f(.default)
|
||||
})))
|
||||
}
|
||||
}
|
||||
f(.default)
|
||||
})))
|
||||
}
|
||||
|
||||
if case .chatList = source, peerGroup != nil {
|
||||
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { _, f in
|
||||
if let chatListController = chatListController {
|
||||
chatListController.deletePeerChat(peerId: peerId, joined: joined)
|
||||
}
|
||||
f(.default)
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
if let item = items.last, case .separator = item {
|
||||
|
@ -173,7 +173,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
||||
self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5)
|
||||
|
||||
self.filterContainerNode = ChatListSearchFiltersContainerNode()
|
||||
self.paneContainerNode = ChatListSearchPaneContainerNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, peersFilter: self.peersFilter, requestPeerType: self.requestPeerType, location: location, searchQuery: self.searchQuery.get(), searchOptions: self.searchOptions.get(), navigationController: navigationController)
|
||||
self.paneContainerNode = ChatListSearchPaneContainerNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, peersFilter: self.peersFilter, requestPeerType: self.requestPeerType, location: location, searchQuery: self.searchQuery.get(), searchOptions: self.searchOptions.get(), navigationController: navigationController, parentController: parentController())
|
||||
self.paneContainerNode.clipsToBounds = true
|
||||
|
||||
super.init()
|
||||
|
@ -289,7 +289,7 @@ private enum ChatListRecentEntry: Comparable, Identifiable {
|
||||
header: header,
|
||||
action: { _ in
|
||||
if let chatPeer = peer.peer.peers[peer.peer.peerId] {
|
||||
peerSelected(EnginePeer(chatPeer), nil, section == .recommendedChannels)
|
||||
peerSelected(EnginePeer(chatPeer), nil, section == .recommendedChannels || section == .popularApps)
|
||||
}
|
||||
},
|
||||
disabledAction: { _ in
|
||||
@ -298,10 +298,18 @@ private enum ChatListRecentEntry: Comparable, Identifiable {
|
||||
}
|
||||
},
|
||||
deletePeer: deletePeer,
|
||||
contextAction: (key == .channels || key == .apps) ? nil : peerContextAction.flatMap { peerContextAction in
|
||||
contextAction: (key == .channels) ? nil : peerContextAction.flatMap { peerContextAction in
|
||||
return { node, gesture, location in
|
||||
if let chatPeer = peer.peer.peers[peer.peer.peerId] {
|
||||
peerContextAction(EnginePeer(chatPeer), .recentSearch, node, gesture, location)
|
||||
let source: ChatListSearchContextActionSource
|
||||
|
||||
if key == .apps {
|
||||
source = .recentApps
|
||||
} else {
|
||||
source = .recentSearch
|
||||
}
|
||||
|
||||
peerContextAction(EnginePeer(chatPeer), source, node, gesture, location)
|
||||
} else {
|
||||
gesture?.cancel()
|
||||
}
|
||||
@ -1072,6 +1080,7 @@ private struct ChatListSearchMessagesContext {
|
||||
public enum ChatListSearchContextActionSource {
|
||||
case recentPeers
|
||||
case recentSearch
|
||||
case recentApps
|
||||
case search(EngineMessage.Id?)
|
||||
}
|
||||
|
||||
@ -1249,6 +1258,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
private let tagMask: EngineMessage.Tags?
|
||||
private let location: ChatListControllerLocation
|
||||
private let navigationController: NavigationController?
|
||||
private weak var parentController: ViewController?
|
||||
|
||||
private let recentListNode: ListView
|
||||
private let shimmerNode: ChatListSearchShimmerNode
|
||||
@ -1321,7 +1331,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
private var searchQueryDisposable: Disposable?
|
||||
private var searchOptionsDisposable: Disposable?
|
||||
|
||||
init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, searchQuery: Signal<String?, NoError>, searchOptions: Signal<ChatListSearchOptions?, NoError>, navigationController: NavigationController?, globalPeerSearchContext: GlobalPeerSearchContext?) {
|
||||
init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, interaction: ChatListSearchInteraction, key: ChatListSearchPaneKey, peersFilter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, searchQuery: Signal<String?, NoError>, searchOptions: Signal<ChatListSearchOptions?, NoError>, navigationController: NavigationController?, parentController: ViewController?, globalPeerSearchContext: GlobalPeerSearchContext?) {
|
||||
self.context = context
|
||||
self.animationCache = animationCache
|
||||
self.animationRenderer = animationRenderer
|
||||
@ -1329,6 +1339,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
self.key = key
|
||||
self.location = location
|
||||
self.navigationController = navigationController
|
||||
self.parentController = parentController
|
||||
|
||||
let globalPeerSearchContext = globalPeerSearchContext ?? GlobalPeerSearchContext()
|
||||
|
||||
@ -3531,22 +3542,32 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
}
|
||||
} else if case .apps = key {
|
||||
if let navigationController = self.navigationController {
|
||||
var customChatNavigationStack: [EnginePeer.Id]?
|
||||
if isRecommended {
|
||||
if let recommendedChannelOrder = previousRecentItemsValue.with({ $0 })?.recommendedChannelOrder {
|
||||
var customChatNavigationStackValue: [EnginePeer.Id] = []
|
||||
customChatNavigationStackValue.append(contentsOf: recommendedChannelOrder)
|
||||
customChatNavigationStack = customChatNavigationStackValue
|
||||
if let peerInfoScreen = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
|
||||
navigationController.pushViewController(peerInfoScreen)
|
||||
}
|
||||
} else if case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), let parentController = self.parentController {
|
||||
self.context.sharedContext.openWebApp(
|
||||
context: self.context,
|
||||
parentController: parentController,
|
||||
updatedPresentationData: nil,
|
||||
peer: peer,
|
||||
threadId: nil,
|
||||
buttonText: "",
|
||||
url: "",
|
||||
simple: true,
|
||||
source: .generic,
|
||||
skipTermsOfService: true
|
||||
)
|
||||
} else {
|
||||
|
||||
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(
|
||||
navigationController: navigationController,
|
||||
context: self.context,
|
||||
chatLocation: .peer(peer),
|
||||
keepStack: .always
|
||||
))
|
||||
}
|
||||
|
||||
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(
|
||||
navigationController: navigationController,
|
||||
context: self.context,
|
||||
chatLocation: .peer(peer),
|
||||
keepStack: .always,
|
||||
customChatNavigationStack: customChatNavigationStack
|
||||
))
|
||||
}
|
||||
} else {
|
||||
interaction.openPeer(peer, nil, threadId, true)
|
||||
|
@ -126,6 +126,7 @@ private final class ChatListSearchPendingPane {
|
||||
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
|
||||
interaction: ChatListSearchInteraction,
|
||||
navigationController: NavigationController?,
|
||||
parentController: ViewController?,
|
||||
peersFilter: ChatListNodePeersFilter,
|
||||
requestPeerType: [ReplyMarkupButtonRequestPeerType]?,
|
||||
location: ChatListControllerLocation,
|
||||
@ -135,7 +136,7 @@ private final class ChatListSearchPendingPane {
|
||||
key: ChatListSearchPaneKey,
|
||||
hasBecomeReady: @escaping (ChatListSearchPaneKey) -> Void
|
||||
) {
|
||||
let paneNode = ChatListSearchListPaneNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, interaction: interaction, key: key, peersFilter: (key == .chats || key == .topics) ? peersFilter : [], requestPeerType: requestPeerType, location: location, searchQuery: searchQuery, searchOptions: searchOptions, navigationController: navigationController, globalPeerSearchContext: globalPeerSearchContext)
|
||||
let paneNode = ChatListSearchListPaneNode(context: context, animationCache: animationCache, animationRenderer: animationRenderer, updatedPresentationData: updatedPresentationData, interaction: interaction, key: key, peersFilter: (key == .chats || key == .topics) ? peersFilter : [], requestPeerType: requestPeerType, location: location, searchQuery: searchQuery, searchOptions: searchOptions, navigationController: navigationController, parentController: parentController, globalPeerSearchContext: globalPeerSearchContext)
|
||||
|
||||
self.pane = ChatListSearchPaneWrapper(key: key, node: paneNode)
|
||||
self.disposable = (paneNode.isReady
|
||||
@ -163,6 +164,7 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerD
|
||||
private let searchOptions: Signal<ChatListSearchOptions?, NoError>
|
||||
private let globalPeerSearchContext: GlobalPeerSearchContext
|
||||
private let navigationController: NavigationController?
|
||||
private weak var parentController: ViewController?
|
||||
var interaction: ChatListSearchInteraction?
|
||||
|
||||
let isReady = Promise<Bool>()
|
||||
@ -193,7 +195,7 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerD
|
||||
|
||||
private var currentAvailablePanes: [ChatListSearchPaneKey]?
|
||||
|
||||
init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peersFilter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, searchQuery: Signal<String?, NoError>, searchOptions: Signal<ChatListSearchOptions?, NoError>, navigationController: NavigationController?) {
|
||||
init(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peersFilter: ChatListNodePeersFilter, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, location: ChatListControllerLocation, searchQuery: Signal<String?, NoError>, searchOptions: Signal<ChatListSearchOptions?, NoError>, navigationController: NavigationController?, parentController: ViewController?) {
|
||||
self.context = context
|
||||
self.animationCache = animationCache
|
||||
self.animationRenderer = animationRenderer
|
||||
@ -204,6 +206,7 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerD
|
||||
self.searchQuery = searchQuery
|
||||
self.searchOptions = searchOptions
|
||||
self.navigationController = navigationController
|
||||
self.parentController = parentController
|
||||
self.globalPeerSearchContext = GlobalPeerSearchContext()
|
||||
|
||||
super.init()
|
||||
@ -434,6 +437,7 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerD
|
||||
updatedPresentationData: self.updatedPresentationData,
|
||||
interaction: self.interaction!,
|
||||
navigationController: self.navigationController,
|
||||
parentController: self.parentController,
|
||||
peersFilter: self.peersFilter,
|
||||
requestPeerType: self.requestPeerType,
|
||||
location: self.location,
|
||||
|
@ -61,12 +61,6 @@ public enum ChatTranslationDisplayType {
|
||||
case translated
|
||||
}
|
||||
|
||||
public enum ChatOpenWebViewSource: Equatable {
|
||||
case generic
|
||||
case menu
|
||||
case inline(bot: EnginePeer)
|
||||
}
|
||||
|
||||
public final class ChatPanelInterfaceInteraction {
|
||||
public let setupReplyMessage: (MessageId?, @escaping (ContainedViewLayoutTransition, @escaping () -> Void) -> Void) -> Void
|
||||
public let setupEditMessage: (MessageId?, @escaping (ContainedViewLayoutTransition) -> Void) -> Void
|
||||
|
@ -842,10 +842,12 @@ private func selectivePrivacySettingsControllerEntries(presentationData: Present
|
||||
}
|
||||
|
||||
if case .phoneNumber = kind, state.setting == .nobody {
|
||||
entries.append(.phoneDiscoveryHeader(presentationData.theme, presentationData.strings.PrivacyPhoneNumberSettings_DiscoveryHeader))
|
||||
entries.append(.phoneDiscoveryEverybody(presentationData.theme, presentationData.strings.PrivacySettings_LastSeenEverybody, state.phoneDiscoveryEnabled != false))
|
||||
entries.append(.phoneDiscoveryMyContacts(presentationData.theme, presentationData.strings.PrivacySettings_LastSeenContacts, state.phoneDiscoveryEnabled == false))
|
||||
entries.append(.phoneDiscoveryInfo(presentationData.theme, state.phoneDiscoveryEnabled != false ? presentationData.strings.PrivacyPhoneNumberSettings_CustomPublicLink("+\(phoneNumber)").string : presentationData.strings.PrivacyPhoneNumberSettings_CustomDisabledHelp, phoneLink))
|
||||
if state.phoneDiscoveryEnabled == false {
|
||||
entries.append(.phoneDiscoveryHeader(presentationData.theme, presentationData.strings.PrivacyPhoneNumberSettings_DiscoveryHeader))
|
||||
entries.append(.phoneDiscoveryEverybody(presentationData.theme, presentationData.strings.PrivacySettings_LastSeenEverybody, state.phoneDiscoveryEnabled != false))
|
||||
entries.append(.phoneDiscoveryMyContacts(presentationData.theme, presentationData.strings.PrivacySettings_LastSeenContacts, state.phoneDiscoveryEnabled == false))
|
||||
entries.append(.phoneDiscoveryInfo(presentationData.theme, state.phoneDiscoveryEnabled != false ? presentationData.strings.PrivacyPhoneNumberSettings_CustomPublicLink("+\(phoneNumber)").string : presentationData.strings.PrivacyPhoneNumberSettings_CustomDisabledHelp, phoneLink))
|
||||
}
|
||||
}
|
||||
|
||||
if case .voiceMessages = kind, !isPremium {
|
||||
|
@ -38,6 +38,7 @@ public protocol SparseItemGridBinding: AnyObject {
|
||||
func unbindLayer(layer: SparseItemGridLayer)
|
||||
func scrollerTextForTag(tag: Int32) -> String?
|
||||
func loadHole(anchor: SparseItemGrid.HoleAnchor, at location: SparseItemGrid.HoleLocation) -> Signal<Never, NoError>
|
||||
func reorderIfPossible(item: SparseItemGrid.Item, toIndex: Int)
|
||||
func onTap(item: SparseItemGrid.Item, itemLayer: CALayer, point: CGPoint)
|
||||
func onTagTap()
|
||||
func didScroll()
|
||||
@ -376,6 +377,48 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var position: CGPoint {
|
||||
get {
|
||||
return self.displayLayer.position
|
||||
} set(value) {
|
||||
if let layer = self.layer {
|
||||
layer.position = value
|
||||
} else if let view = self.view {
|
||||
view.center = value
|
||||
} else {
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var bounds: CGRect {
|
||||
get {
|
||||
return self.displayLayer.bounds
|
||||
} set(value) {
|
||||
if let layer = self.layer {
|
||||
layer.bounds = value
|
||||
} else if let view = self.view {
|
||||
view.bounds = value
|
||||
} else {
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var transform: CATransform3D {
|
||||
get {
|
||||
return self.displayLayer.transform
|
||||
} set(value) {
|
||||
if let layer = self.layer {
|
||||
layer.transform = value
|
||||
} else if let view = self.view {
|
||||
view.layer.transform = value
|
||||
} else {
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var needsShimmer: Bool {
|
||||
if let layer = self.layer {
|
||||
@ -485,6 +528,8 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
var items: Items?
|
||||
var visibleItems: [AnyHashable: VisibleItem] = [:]
|
||||
var visiblePlaceholders: [SparseItemGridShimmerLayer] = []
|
||||
|
||||
private var reorderingItem: (id: AnyHashable, initialPosition: CGPoint, position: CGPoint)?
|
||||
|
||||
private var scrollingArea: SparseItemGridScrollingArea?
|
||||
private var currentScrollingTag: Int32?
|
||||
@ -492,6 +537,8 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
|
||||
private var ignoreScrolling: Bool = false
|
||||
private var isFastScrolling: Bool = false
|
||||
|
||||
private var isReordering: Bool = false
|
||||
|
||||
private var previousScrollOffset: CGFloat = 0.0
|
||||
var coveringInsetOffset: CGFloat = 0.0
|
||||
@ -532,16 +579,68 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
self.view.addSubview(self.scrollView)
|
||||
}
|
||||
|
||||
func update(containerLayout: ContainerLayout, items: Items, restoreScrollPosition: (y: CGFloat, index: Int)?, synchronous: SparseItemGrid.Synchronous) {
|
||||
func update(containerLayout: ContainerLayout, items: Items, restoreScrollPosition: (y: CGFloat, index: Int)?, synchronous: SparseItemGrid.Synchronous, transition: ComponentTransition) {
|
||||
if self.layout?.containerLayout != containerLayout || self.items !== items {
|
||||
self.layout = Layout(containerLayout: containerLayout, zoomLevel: self.zoomLevel, itemCount: items.count)
|
||||
self.items = items
|
||||
|
||||
self.updateVisibleItems(resetScrolling: true, synchronous: synchronous, restoreScrollPosition: restoreScrollPosition)
|
||||
self.updateVisibleItems(resetScrolling: true, synchronous: synchronous, restoreScrollPosition: restoreScrollPosition, transition: transition)
|
||||
|
||||
self.snapCoveringInsetOffset(animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
func setReordering(isReordering: Bool) {
|
||||
if self.isReordering != isReordering {
|
||||
self.isReordering = isReordering
|
||||
|
||||
self.updateVisibleItems(resetScrolling: true, synchronous: .semi, restoreScrollPosition: nil, transition: .spring(duration: 0.4))
|
||||
}
|
||||
}
|
||||
|
||||
func setReorderingItem(item: SparseItemGridDisplayItem?) {
|
||||
var mappedItem: (AnyHashable, VisibleItem)?
|
||||
if let item, let itemLayer = item.layer {
|
||||
for (id, visibleItem) in self.visibleItems {
|
||||
if visibleItem.layer === itemLayer {
|
||||
mappedItem = (id, visibleItem)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.reorderingItem?.id != mappedItem?.0 {
|
||||
if let (id, visibleItem) = mappedItem, let itemLayer = visibleItem.layer {
|
||||
self.scrollView.layer.addSublayer(itemLayer)
|
||||
self.reorderingItem = (id, itemLayer.position, itemLayer.position)
|
||||
} else {
|
||||
self.reorderingItem = nil
|
||||
}
|
||||
self.updateVisibleItems(resetScrolling: true, synchronous: .semi, restoreScrollPosition: nil, transition: .spring(duration: 0.4))
|
||||
}
|
||||
}
|
||||
|
||||
func moveReorderingItem(distance: CGPoint) {
|
||||
if let (id, initialPosition, _) = self.reorderingItem {
|
||||
let targetPosition = CGPoint(x: initialPosition.x + distance.x, y: initialPosition.y + distance.y)
|
||||
self.reorderingItem = (id, initialPosition, targetPosition)
|
||||
self.updateVisibleItems(resetScrolling: true, synchronous: .semi, restoreScrollPosition: nil, transition: .immediate)
|
||||
|
||||
if let items = self.items, let visibleReorderingItem = self.visibleItems[id] {
|
||||
for (visibleId, visibleItem) in self.visibleItems {
|
||||
if visibleItem === visibleReorderingItem {
|
||||
continue
|
||||
}
|
||||
if visibleItem.frame.contains(targetPosition) {
|
||||
if let item = items.items.first(where: { $0.id == id }), let targetItem = items.items.first(where: { $0.id == visibleId }) {
|
||||
items.itemBinding.reorderIfPossible(item: item, toIndex: targetItem.index)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
self.items?.itemBinding.didScroll()
|
||||
@ -554,7 +653,7 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
|
||||
@objc func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if !self.ignoreScrolling {
|
||||
self.updateVisibleItems(resetScrolling: false, synchronous: .full, restoreScrollPosition: nil)
|
||||
self.updateVisibleItems(resetScrolling: false, synchronous: .full, restoreScrollPosition: nil, transition: .immediate)
|
||||
|
||||
if let layout = self.layout, let _ = self.items {
|
||||
let offset = scrollView.contentOffset.y
|
||||
@ -916,10 +1015,10 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
}
|
||||
|
||||
func updateShimmerColors() {
|
||||
self.updateVisibleItems(resetScrolling: false, synchronous: .none, restoreScrollPosition: nil)
|
||||
self.updateVisibleItems(resetScrolling: false, synchronous: .none, restoreScrollPosition: nil, transition: .immediate)
|
||||
}
|
||||
|
||||
private func updateVisibleItems(resetScrolling: Bool, synchronous: SparseItemGrid.Synchronous, restoreScrollPosition: (y: CGFloat, index: Int)?) {
|
||||
private func updateVisibleItems(resetScrolling: Bool, synchronous: SparseItemGrid.Synchronous, restoreScrollPosition: (y: CGFloat, index: Int)?, transition: ComponentTransition) {
|
||||
guard let layout = self.layout, let items = self.items else {
|
||||
return
|
||||
}
|
||||
@ -980,23 +1079,32 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
|
||||
let visibleRange = layout.visibleItemRange(for: visibleBounds, count: items.count)
|
||||
if visibleRange.maxIndex >= visibleRange.minIndex {
|
||||
for index in visibleRange.minIndex ... visibleRange.maxIndex {
|
||||
let processItemAtIndex: (Int) -> Void = { index in
|
||||
if let item = items.item(at: index) {
|
||||
let itemFrame = layout.frame(at: index)
|
||||
var itemFrame = layout.frame(at: index)
|
||||
|
||||
let itemLayer: VisibleItem
|
||||
var isNewlyAdded = false
|
||||
if let current = self.visibleItems[item.id] {
|
||||
itemLayer = current
|
||||
updateLayers.append((itemLayer, index))
|
||||
} else {
|
||||
isNewlyAdded = true
|
||||
itemLayer = VisibleItem(layer: items.itemBinding.createLayer(item: item), view: items.itemBinding.createView())
|
||||
|
||||
itemLayer.layer?.masksToBounds = true
|
||||
|
||||
self.visibleItems[item.id] = itemLayer
|
||||
|
||||
bindItems.append(item)
|
||||
bindLayers.append(itemLayer)
|
||||
|
||||
if let layer = itemLayer.layer {
|
||||
self.scrollView.layer.addSublayer(layer)
|
||||
if let reorderingItem = self.reorderingItem, let visibleReorderingItem = self.visibleItems[reorderingItem.id] {
|
||||
self.scrollView.layer.insertSublayer(layer, below: visibleReorderingItem.layer)
|
||||
} else {
|
||||
self.scrollView.layer.addSublayer(layer)
|
||||
}
|
||||
} else if let view = itemLayer.view {
|
||||
self.scrollView.addSubview(view)
|
||||
}
|
||||
@ -1038,9 +1146,55 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
|
||||
validIds.insert(item.id)
|
||||
|
||||
itemLayer.frame = itemFrame
|
||||
if let blurLayer = itemLayer.blurLayer {
|
||||
blurLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: itemFrame.minY), size: CGSize(width: layout.containerLayout.size.width, height: itemFrame.height))
|
||||
var itemScale: CGFloat
|
||||
let itemCornerRadius: CGFloat
|
||||
if self.isReordering {
|
||||
itemScale = (itemFrame.height - 6.0 * 2.0) / itemFrame.height
|
||||
itemCornerRadius = 10.0
|
||||
} else {
|
||||
itemScale = 1.0
|
||||
itemCornerRadius = 0.0
|
||||
}
|
||||
|
||||
let itemAlpha: CGFloat
|
||||
if let reorderingItem = self.reorderingItem, item.id == reorderingItem.id {
|
||||
itemAlpha = 0.8
|
||||
itemScale = 0.9
|
||||
itemFrame = itemFrame.size.centered(around: reorderingItem.position)
|
||||
} else {
|
||||
itemAlpha = 1.0
|
||||
}
|
||||
|
||||
if transition.animation.isImmediate || isNewlyAdded {
|
||||
itemLayer.position = itemFrame.center
|
||||
itemLayer.bounds = CGRect(origin: CGPoint(), size: itemFrame.size)
|
||||
itemLayer.transform = CATransform3DMakeScale(itemScale, itemScale, 1.0)
|
||||
itemLayer.layer?.cornerRadius = itemCornerRadius
|
||||
itemLayer.layer?.opacity = Float(itemAlpha)
|
||||
if let blurLayer = itemLayer.blurLayer {
|
||||
blurLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: itemFrame.minY), size: CGSize(width: layout.containerLayout.size.width, height: itemFrame.height))
|
||||
}
|
||||
} else {
|
||||
if let itemLayerValue = itemLayer.layer {
|
||||
transition.setPosition(layer: itemLayerValue, position: itemFrame.center)
|
||||
transition.setBounds(layer: itemLayerValue, bounds: CGRect(origin: CGPoint(), size: itemFrame.size))
|
||||
transition.setTransform(layer: itemLayerValue, transform: CATransform3DMakeScale(itemScale, itemScale, 1.0))
|
||||
transition.setCornerRadius(layer: itemLayerValue, cornerRadius: itemCornerRadius)
|
||||
transition.setAlpha(layer: itemLayerValue, alpha: itemAlpha)
|
||||
|
||||
if let blurLayer = itemLayer.blurLayer {
|
||||
blurLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: itemFrame.minY), size: CGSize(width: layout.containerLayout.size.width, height: itemFrame.height))
|
||||
}
|
||||
} else {
|
||||
itemLayer.position = itemFrame.center
|
||||
itemLayer.bounds = CGRect(origin: CGPoint(), size: itemFrame.size)
|
||||
itemLayer.transform = CATransform3DMakeScale(itemScale, itemScale, 1.0)
|
||||
itemLayer.layer?.cornerRadius = itemCornerRadius
|
||||
itemLayer.layer?.opacity = Float(itemAlpha)
|
||||
if let blurLayer = itemLayer.blurLayer {
|
||||
blurLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: itemFrame.minY), size: CGSize(width: layout.containerLayout.size.width, height: itemFrame.height))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let placeholderLayer: SparseItemGridShimmerLayer
|
||||
@ -1058,6 +1212,22 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
usedPlaceholderCount += 1
|
||||
}
|
||||
}
|
||||
for index in visibleRange.minIndex ... visibleRange.maxIndex {
|
||||
processItemAtIndex(index)
|
||||
}
|
||||
if let reorderingItem = self.reorderingItem, let items = self.items {
|
||||
var reorderingItemIndex: Int?
|
||||
for item in items.items {
|
||||
if item.id == reorderingItem.id {
|
||||
reorderingItemIndex = item.index
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if let reorderingItemIndex, !(visibleRange.minIndex ... visibleRange.maxIndex).contains(reorderingItemIndex) {
|
||||
processItemAtIndex(reorderingItemIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !bindItems.isEmpty {
|
||||
@ -1068,9 +1238,20 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
let item = item as! VisibleItem
|
||||
let contentItem = items.item(at: index)
|
||||
if let layer = item.layer {
|
||||
layer.update(size: layer.frame.size, insets: layout.containerLayout.insets, displayItem: item, binding: items.itemBinding, item: contentItem)
|
||||
layer.update(size: layer.bounds.size, insets: layout.containerLayout.insets, displayItem: item, binding: items.itemBinding, item: contentItem)
|
||||
|
||||
if self.isReordering {
|
||||
if layer.animation(forKey: "shaking_position") == nil {
|
||||
startShaking(layer: layer)
|
||||
}
|
||||
} else {
|
||||
if layer.animation(forKey: "shaking_position") != nil {
|
||||
layer.removeAnimation(forKey: "shaking_position")
|
||||
layer.removeAnimation(forKey: "shaking_rotation")
|
||||
}
|
||||
}
|
||||
} else if let view = item.view {
|
||||
view.update(size: view.layer.frame.size, insets: layout.containerLayout.insets)
|
||||
view.update(size: view.layer.bounds.size, insets: layout.containerLayout.insets)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1418,6 +1599,9 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
|
||||
private var tapRecognizer: UITapGestureRecognizer?
|
||||
private var pinchRecognizer: UIPinchGestureRecognizer?
|
||||
|
||||
private var isReordering: Bool = false
|
||||
private var reorderRecognizer: ReorderGestureRecognizer?
|
||||
|
||||
private var theme: PresentationTheme
|
||||
private var containerLayout: ContainerLayout?
|
||||
@ -1492,6 +1676,41 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
let pinchRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.pinchGesture(_:)))
|
||||
self.pinchRecognizer = pinchRecognizer
|
||||
self.view.addGestureRecognizer(pinchRecognizer)
|
||||
|
||||
let reorderRecognizer = ReorderGestureRecognizer(
|
||||
shouldBegin: { [weak self] point in
|
||||
guard let self, let item = self.item(at: point) else {
|
||||
return (allowed: false, requiresLongPress: false, item: nil)
|
||||
}
|
||||
|
||||
return (allowed: true, requiresLongPress: false, item: item)
|
||||
},
|
||||
willBegin: { point in
|
||||
},
|
||||
began: { [weak self] item in
|
||||
guard let self, let currentViewport = self.currentViewport else {
|
||||
return
|
||||
}
|
||||
currentViewport.setReorderingItem(item: item)
|
||||
},
|
||||
ended: { [weak self] in
|
||||
guard let self, let currentViewport = self.currentViewport else {
|
||||
return
|
||||
}
|
||||
currentViewport.setReorderingItem(item: nil)
|
||||
},
|
||||
moved: { [weak self] distance in
|
||||
guard let self, let currentViewport = self.currentViewport else {
|
||||
return
|
||||
}
|
||||
currentViewport.moveReorderingItem(distance: distance)
|
||||
},
|
||||
isActiveUpdated: { _ in
|
||||
}
|
||||
)
|
||||
self.reorderRecognizer = reorderRecognizer
|
||||
self.view.addGestureRecognizer(reorderRecognizer)
|
||||
reorderRecognizer.isEnabled = false
|
||||
|
||||
self.addSubnode(self.scrollingArea)
|
||||
self.scrollingArea.openCurrentDate = { [weak self] in
|
||||
@ -1584,7 +1803,7 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
})
|
||||
|
||||
nextViewport.frame = CGRect(origin: CGPoint(), size: containerLayout.size)
|
||||
nextViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: restoreScrollPosition, synchronous: .semi)
|
||||
nextViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: restoreScrollPosition, synchronous: .semi, transition: .immediate)
|
||||
|
||||
self.currentViewportTransition?.removeFromSupernode()
|
||||
|
||||
@ -1638,7 +1857,7 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
})
|
||||
|
||||
nextViewport.frame = CGRect(origin: CGPoint(), size: containerLayout.size)
|
||||
nextViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: restoreScrollPosition, synchronous: .semi)
|
||||
nextViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: restoreScrollPosition, synchronous: .semi, transition: .immediate)
|
||||
|
||||
let currentViewportTransition = ViewportTransition(interactiveState: interactiveState, layout: containerLayout, anchorItemIndex: anchorItemIndex, transitionAnchorPoint: anchorLocation, from: previousViewport, to: nextViewport, coveringOffsetUpdated: { [weak self] transition in
|
||||
self?.transitionCoveringOffsetUpdated(transition: transition)
|
||||
@ -1679,7 +1898,7 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
strongSelf.scrollingArea.frame = CGRect(origin: CGPoint(), size: containerLayout.size)
|
||||
currentViewport.setScrollingArea(scrollingArea: strongSelf.scrollingArea)
|
||||
currentViewport.frame = CGRect(origin: CGPoint(), size: containerLayout.size)
|
||||
currentViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: nil, synchronous: .semi)
|
||||
currentViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: nil, synchronous: .semi, transition: .immediate)
|
||||
}
|
||||
|
||||
strongSelf.currentViewportTransition = nil
|
||||
@ -1691,7 +1910,7 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
public func update(size: CGSize, insets: UIEdgeInsets, useSideInsets: Bool, scrollIndicatorInsets: UIEdgeInsets, lockScrollingAtTop: Bool, fixedItemHeight: CGFloat?, fixedItemAspect: CGFloat?, items: Items, theme: PresentationTheme, synchronous: SparseItemGrid.Synchronous) {
|
||||
public func update(size: CGSize, insets: UIEdgeInsets, useSideInsets: Bool, scrollIndicatorInsets: UIEdgeInsets, lockScrollingAtTop: Bool, fixedItemHeight: CGFloat?, fixedItemAspect: CGFloat?, items: Items, theme: PresentationTheme, synchronous: SparseItemGrid.Synchronous, transition: ComponentTransition = .immediate) {
|
||||
self.theme = theme
|
||||
|
||||
var headerInset: CGFloat = 0.0
|
||||
@ -1737,9 +1956,15 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
self.items = items
|
||||
self.scrollingArea.isHidden = lockScrollingAtTop
|
||||
|
||||
self.tapRecognizer?.isEnabled = fixedItemHeight == nil
|
||||
self.pinchRecognizer?.isEnabled = fixedItemHeight == nil
|
||||
|
||||
if self.isReordering {
|
||||
self.tapRecognizer?.isEnabled = false
|
||||
self.pinchRecognizer?.isEnabled = false
|
||||
self.reorderRecognizer?.isEnabled = true
|
||||
} else {
|
||||
self.tapRecognizer?.isEnabled = fixedItemHeight == nil
|
||||
self.pinchRecognizer?.isEnabled = fixedItemHeight == nil
|
||||
self.reorderRecognizer?.isEnabled = false
|
||||
}
|
||||
|
||||
if self.currentViewport == nil {
|
||||
let currentViewport = Viewport(theme: self.theme, zoomLevel: self.initialZoomLevel ?? ZoomLevel(rawValue: 3), maybeLoadHoleAnchor: { [weak self] holeAnchor, location in
|
||||
@ -1762,7 +1987,7 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
} else if let currentViewport = self.currentViewport {
|
||||
self.scrollingArea.frame = CGRect(origin: CGPoint(), size: size)
|
||||
currentViewport.frame = CGRect(origin: CGPoint(), size: size)
|
||||
currentViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: nil, synchronous: synchronous)
|
||||
currentViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: nil, synchronous: synchronous, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1841,7 +2066,7 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
|
||||
self.scrollingArea.frame = CGRect(origin: CGPoint(), size: containerLayout.size)
|
||||
currentViewport.frame = CGRect(origin: CGPoint(), size: containerLayout.size)
|
||||
currentViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: restoreScrollPosition, synchronous: .semi)
|
||||
currentViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: restoreScrollPosition, synchronous: .semi, transition: .immediate)
|
||||
|
||||
let currentViewportTransition = ViewportTransition(interactiveState: nil, layout: containerLayout, anchorItemIndex: anchorItemIndex, transitionAnchorPoint: anchorLocation, from: previousViewport, to: currentViewport, coveringOffsetUpdated: { [weak self] transition in
|
||||
self?.transitionCoveringOffsetUpdated(transition: transition)
|
||||
@ -1861,7 +2086,7 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
strongSelf.insertSubnode(currentViewport, belowSubnode: strongSelf.scrollingArea)
|
||||
strongSelf.scrollingArea.frame = CGRect(origin: CGPoint(), size: containerLayout.size)
|
||||
currentViewport.frame = CGRect(origin: CGPoint(), size: containerLayout.size)
|
||||
currentViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: nil, synchronous: .semi)
|
||||
currentViewport.update(containerLayout: containerLayout, items: items, restoreScrollPosition: nil, synchronous: .semi, transition: .immediate)
|
||||
}
|
||||
|
||||
strongSelf.currentViewport?.setScrollingArea(scrollingArea: strongSelf.scrollingArea)
|
||||
@ -1874,6 +2099,24 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func setReordering(isReordering: Bool) {
|
||||
self.isReordering = isReordering
|
||||
|
||||
if let currentViewport = self.currentViewport {
|
||||
currentViewport.setReordering(isReordering: isReordering)
|
||||
}
|
||||
|
||||
if self.isReordering {
|
||||
self.tapRecognizer?.isEnabled = false
|
||||
self.pinchRecognizer?.isEnabled = false
|
||||
self.reorderRecognizer?.isEnabled = true
|
||||
} else {
|
||||
self.tapRecognizer?.isEnabled = self.containerLayout?.fixedItemHeight == nil
|
||||
self.pinchRecognizer?.isEnabled = self.containerLayout?.fixedItemHeight == nil
|
||||
self.reorderRecognizer?.isEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
private func coveringOffsetUpdated(viewport: Viewport, transition: ContainedViewLayoutTransition) {
|
||||
guard let items = self.items else {
|
||||
@ -2050,3 +2293,244 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func startShaking(layer: CALayer) {
|
||||
func degreesToRadians(_ x: CGFloat) -> CGFloat {
|
||||
return .pi * x / 180.0
|
||||
}
|
||||
|
||||
let duration: Double = 0.4
|
||||
let displacement: CGFloat = 1.0
|
||||
let degreesRotation: CGFloat = 2.0
|
||||
|
||||
let negativeDisplacement = -1.0 * displacement
|
||||
let position = CAKeyframeAnimation.init(keyPath: "position")
|
||||
position.beginTime = 0.8
|
||||
position.duration = duration
|
||||
position.values = [
|
||||
NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)),
|
||||
NSValue(cgPoint: CGPoint(x: 0, y: 0)),
|
||||
NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: 0)),
|
||||
NSValue(cgPoint: CGPoint(x: 0, y: negativeDisplacement)),
|
||||
NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement))
|
||||
]
|
||||
position.calculationMode = .linear
|
||||
position.isRemovedOnCompletion = false
|
||||
position.repeatCount = Float.greatestFiniteMagnitude
|
||||
position.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100))
|
||||
position.isAdditive = true
|
||||
|
||||
let transform = CAKeyframeAnimation.init(keyPath: "transform")
|
||||
transform.beginTime = 2.6
|
||||
transform.duration = 0.3
|
||||
transform.valueFunction = CAValueFunction(name: CAValueFunctionName.rotateZ)
|
||||
transform.values = [
|
||||
degreesToRadians(-1.0 * degreesRotation),
|
||||
degreesToRadians(degreesRotation),
|
||||
degreesToRadians(-1.0 * degreesRotation)
|
||||
]
|
||||
transform.calculationMode = .linear
|
||||
transform.isRemovedOnCompletion = false
|
||||
transform.repeatCount = Float.greatestFiniteMagnitude
|
||||
transform.isAdditive = true
|
||||
transform.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100))
|
||||
|
||||
layer.add(position, forKey: "shaking_position")
|
||||
layer.add(transform, forKey: "shaking_rotation")
|
||||
}
|
||||
|
||||
private final class ReorderGestureRecognizer: UIGestureRecognizer {
|
||||
private let shouldBegin: (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, item: SparseItemGridDisplayItem?)
|
||||
private let willBegin: (CGPoint) -> Void
|
||||
private let began: (SparseItemGridDisplayItem) -> Void
|
||||
private let ended: () -> Void
|
||||
private let moved: (CGPoint) -> Void
|
||||
private let isActiveUpdated: (Bool) -> Void
|
||||
|
||||
private var initialLocation: CGPoint?
|
||||
private var longTapTimer: SwiftSignalKit.Timer?
|
||||
private var longPressTimer: SwiftSignalKit.Timer?
|
||||
|
||||
private var itemView: SparseItemGridDisplayItem?
|
||||
|
||||
public init(shouldBegin: @escaping (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, item: SparseItemGridDisplayItem?), willBegin: @escaping (CGPoint) -> Void, began: @escaping (SparseItemGridDisplayItem) -> Void, ended: @escaping () -> Void, moved: @escaping (CGPoint) -> Void, isActiveUpdated: @escaping (Bool) -> Void) {
|
||||
self.shouldBegin = shouldBegin
|
||||
self.willBegin = willBegin
|
||||
self.began = began
|
||||
self.ended = ended
|
||||
self.moved = moved
|
||||
self.isActiveUpdated = isActiveUpdated
|
||||
|
||||
super.init(target: nil, action: nil)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.longTapTimer?.invalidate()
|
||||
self.longPressTimer?.invalidate()
|
||||
}
|
||||
|
||||
private func startLongTapTimer() {
|
||||
self.longTapTimer?.invalidate()
|
||||
let longTapTimer = SwiftSignalKit.Timer(timeout: 0.25, repeat: false, completion: { [weak self] in
|
||||
self?.longTapTimerFired()
|
||||
}, queue: Queue.mainQueue())
|
||||
self.longTapTimer = longTapTimer
|
||||
longTapTimer.start()
|
||||
}
|
||||
|
||||
private func stopLongTapTimer() {
|
||||
self.itemView = nil
|
||||
self.longTapTimer?.invalidate()
|
||||
self.longTapTimer = nil
|
||||
}
|
||||
|
||||
private func startLongPressTimer() {
|
||||
self.longPressTimer?.invalidate()
|
||||
let longPressTimer = SwiftSignalKit.Timer(timeout: 0.6, repeat: false, completion: { [weak self] in
|
||||
self?.longPressTimerFired()
|
||||
}, queue: Queue.mainQueue())
|
||||
self.longPressTimer = longPressTimer
|
||||
longPressTimer.start()
|
||||
}
|
||||
|
||||
private func stopLongPressTimer() {
|
||||
self.itemView = nil
|
||||
self.longPressTimer?.invalidate()
|
||||
self.longPressTimer = nil
|
||||
}
|
||||
|
||||
override public func reset() {
|
||||
super.reset()
|
||||
|
||||
self.itemView = nil
|
||||
self.stopLongTapTimer()
|
||||
self.stopLongPressTimer()
|
||||
self.initialLocation = nil
|
||||
|
||||
self.isActiveUpdated(false)
|
||||
}
|
||||
|
||||
private func longTapTimerFired() {
|
||||
guard let location = self.initialLocation else {
|
||||
return
|
||||
}
|
||||
|
||||
self.longTapTimer?.invalidate()
|
||||
self.longTapTimer = nil
|
||||
|
||||
self.willBegin(location)
|
||||
}
|
||||
|
||||
private func longPressTimerFired() {
|
||||
guard let _ = self.initialLocation else {
|
||||
return
|
||||
}
|
||||
|
||||
self.isActiveUpdated(true)
|
||||
self.state = .began
|
||||
self.longPressTimer?.invalidate()
|
||||
self.longPressTimer = nil
|
||||
self.longTapTimer?.invalidate()
|
||||
self.longTapTimer = nil
|
||||
if let itemView = self.itemView {
|
||||
self.began(itemView)
|
||||
}
|
||||
self.isActiveUpdated(true)
|
||||
}
|
||||
|
||||
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
|
||||
if self.numberOfTouches > 1 {
|
||||
self.isActiveUpdated(false)
|
||||
self.state = .failed
|
||||
self.ended()
|
||||
return
|
||||
}
|
||||
|
||||
if self.state == .possible {
|
||||
if let location = touches.first?.location(in: self.view) {
|
||||
let (allowed, requiresLongPress, itemView) = self.shouldBegin(location)
|
||||
if allowed {
|
||||
self.isActiveUpdated(true)
|
||||
|
||||
self.itemView = itemView
|
||||
self.initialLocation = location
|
||||
if requiresLongPress {
|
||||
self.startLongTapTimer()
|
||||
self.startLongPressTimer()
|
||||
} else {
|
||||
self.state = .began
|
||||
if let itemView = self.itemView {
|
||||
self.began(itemView)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.isActiveUpdated(false)
|
||||
self.state = .failed
|
||||
}
|
||||
} else {
|
||||
self.isActiveUpdated(false)
|
||||
self.state = .failed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesEnded(touches, with: event)
|
||||
|
||||
self.initialLocation = nil
|
||||
|
||||
self.stopLongTapTimer()
|
||||
if self.longPressTimer != nil {
|
||||
self.stopLongPressTimer()
|
||||
self.isActiveUpdated(false)
|
||||
self.state = .failed
|
||||
}
|
||||
if self.state == .began || self.state == .changed {
|
||||
self.isActiveUpdated(false)
|
||||
self.ended()
|
||||
self.state = .failed
|
||||
}
|
||||
}
|
||||
|
||||
override public func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesCancelled(touches, with: event)
|
||||
|
||||
self.initialLocation = nil
|
||||
|
||||
self.stopLongTapTimer()
|
||||
if self.longPressTimer != nil {
|
||||
self.isActiveUpdated(false)
|
||||
self.stopLongPressTimer()
|
||||
self.state = .failed
|
||||
}
|
||||
if self.state == .began || self.state == .changed {
|
||||
self.isActiveUpdated(false)
|
||||
self.ended()
|
||||
self.state = .failed
|
||||
}
|
||||
}
|
||||
|
||||
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesMoved(touches, with: event)
|
||||
|
||||
if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) {
|
||||
self.state = .changed
|
||||
let offset = CGPoint(x: location.x - initialLocation.x, y: location.y - initialLocation.y)
|
||||
self.moved(offset)
|
||||
} else if let touch = touches.first, let initialTapLocation = self.initialLocation, self.longPressTimer != nil {
|
||||
let touchLocation = touch.location(in: self.view)
|
||||
let dX = touchLocation.x - initialTapLocation.x
|
||||
let dY = touchLocation.y - initialTapLocation.y
|
||||
|
||||
if dX * dX + dY * dY > 3.0 * 3.0 {
|
||||
self.stopLongTapTimer()
|
||||
self.stopLongPressTimer()
|
||||
self.initialLocation = nil
|
||||
self.isActiveUpdated(false)
|
||||
self.state = .failed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1081,7 +1081,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[-1886646706] = { return Api.UrlAuthResult.parse_urlAuthResultAccepted($0) }
|
||||
dict[-1445536993] = { return Api.UrlAuthResult.parse_urlAuthResultDefault($0) }
|
||||
dict[-1831650802] = { return Api.UrlAuthResult.parse_urlAuthResultRequest($0) }
|
||||
dict[1340722400] = { return Api.User.parse_user($0) }
|
||||
dict[-2093920310] = { return Api.User.parse_user($0) }
|
||||
dict[-742634630] = { return Api.User.parse_userEmpty($0) }
|
||||
dict[-862357728] = { return Api.UserFull.parse_userFull($0) }
|
||||
dict[-2100168954] = { return Api.UserProfilePhoto.parse_userProfilePhoto($0) }
|
||||
@ -1399,7 +1399,7 @@ public extension Api {
|
||||
return parser(reader)
|
||||
}
|
||||
else {
|
||||
telegramApiLog("Type constructor \(String(signature, radix: 16, uppercase: false)) not found")
|
||||
telegramApiLog("Type constructor \(String(UInt32(bitPattern: signature), radix: 16, uppercase: false)) not found")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -452,14 +452,14 @@ public extension Api {
|
||||
}
|
||||
public extension Api {
|
||||
enum User: TypeConstructorDescription {
|
||||
case user(flags: Int32, flags2: Int32, id: Int64, accessHash: Int64?, firstName: String?, lastName: String?, username: String?, phone: String?, photo: Api.UserProfilePhoto?, status: Api.UserStatus?, botInfoVersion: Int32?, restrictionReason: [Api.RestrictionReason]?, botInlinePlaceholder: String?, langCode: String?, emojiStatus: Api.EmojiStatus?, usernames: [Api.Username]?, storiesMaxId: Int32?, color: Api.PeerColor?, profileColor: Api.PeerColor?, botDailyUsers: Int32?)
|
||||
case user(flags: Int32, flags2: Int32, id: Int64, accessHash: Int64?, firstName: String?, lastName: String?, username: String?, phone: String?, photo: Api.UserProfilePhoto?, status: Api.UserStatus?, botInfoVersion: Int32?, restrictionReason: [Api.RestrictionReason]?, botInlinePlaceholder: String?, langCode: String?, emojiStatus: Api.EmojiStatus?, usernames: [Api.Username]?, storiesMaxId: Int32?, color: Api.PeerColor?, profileColor: Api.PeerColor?, botActiveUsers: Int32?)
|
||||
case userEmpty(id: Int64)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
case .user(let flags, let flags2, let id, let accessHash, let firstName, let lastName, let username, let phone, let photo, let status, let botInfoVersion, let restrictionReason, let botInlinePlaceholder, let langCode, let emojiStatus, let usernames, let storiesMaxId, let color, let profileColor, let botDailyUsers):
|
||||
case .user(let flags, let flags2, let id, let accessHash, let firstName, let lastName, let username, let phone, let photo, let status, let botInfoVersion, let restrictionReason, let botInlinePlaceholder, let langCode, let emojiStatus, let usernames, let storiesMaxId, let color, let profileColor, let botActiveUsers):
|
||||
if boxed {
|
||||
buffer.appendInt32(1340722400)
|
||||
buffer.appendInt32(-2093920310)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
serializeInt32(flags2, buffer: buffer, boxed: false)
|
||||
@ -488,7 +488,7 @@ public extension Api {
|
||||
if Int(flags2) & Int(1 << 5) != 0 {serializeInt32(storiesMaxId!, buffer: buffer, boxed: false)}
|
||||
if Int(flags2) & Int(1 << 8) != 0 {color!.serialize(buffer, true)}
|
||||
if Int(flags2) & Int(1 << 9) != 0 {profileColor!.serialize(buffer, true)}
|
||||
if Int(flags2) & Int(1 << 12) != 0 {serializeInt32(botDailyUsers!, buffer: buffer, boxed: false)}
|
||||
if Int(flags2) & Int(1 << 12) != 0 {serializeInt32(botActiveUsers!, buffer: buffer, boxed: false)}
|
||||
break
|
||||
case .userEmpty(let id):
|
||||
if boxed {
|
||||
@ -501,8 +501,8 @@ public extension Api {
|
||||
|
||||
public func descriptionFields() -> (String, [(String, Any)]) {
|
||||
switch self {
|
||||
case .user(let flags, let flags2, let id, let accessHash, let firstName, let lastName, let username, let phone, let photo, let status, let botInfoVersion, let restrictionReason, let botInlinePlaceholder, let langCode, let emojiStatus, let usernames, let storiesMaxId, let color, let profileColor, let botDailyUsers):
|
||||
return ("user", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("firstName", firstName as Any), ("lastName", lastName as Any), ("username", username as Any), ("phone", phone as Any), ("photo", photo as Any), ("status", status as Any), ("botInfoVersion", botInfoVersion as Any), ("restrictionReason", restrictionReason as Any), ("botInlinePlaceholder", botInlinePlaceholder as Any), ("langCode", langCode as Any), ("emojiStatus", emojiStatus as Any), ("usernames", usernames as Any), ("storiesMaxId", storiesMaxId as Any), ("color", color as Any), ("profileColor", profileColor as Any), ("botDailyUsers", botDailyUsers as Any)])
|
||||
case .user(let flags, let flags2, let id, let accessHash, let firstName, let lastName, let username, let phone, let photo, let status, let botInfoVersion, let restrictionReason, let botInlinePlaceholder, let langCode, let emojiStatus, let usernames, let storiesMaxId, let color, let profileColor, let botActiveUsers):
|
||||
return ("user", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("firstName", firstName as Any), ("lastName", lastName as Any), ("username", username as Any), ("phone", phone as Any), ("photo", photo as Any), ("status", status as Any), ("botInfoVersion", botInfoVersion as Any), ("restrictionReason", restrictionReason as Any), ("botInlinePlaceholder", botInlinePlaceholder as Any), ("langCode", langCode as Any), ("emojiStatus", emojiStatus as Any), ("usernames", usernames as Any), ("storiesMaxId", storiesMaxId as Any), ("color", color as Any), ("profileColor", profileColor as Any), ("botActiveUsers", botActiveUsers as Any)])
|
||||
case .userEmpty(let id):
|
||||
return ("userEmpty", [("id", id as Any)])
|
||||
}
|
||||
@ -584,7 +584,7 @@ public extension Api {
|
||||
let _c19 = (Int(_2!) & Int(1 << 9) == 0) || _19 != nil
|
||||
let _c20 = (Int(_2!) & Int(1 << 12) == 0) || _20 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 {
|
||||
return Api.User.user(flags: _1!, flags2: _2!, id: _3!, accessHash: _4, firstName: _5, lastName: _6, username: _7, phone: _8, photo: _9, status: _10, botInfoVersion: _11, restrictionReason: _12, botInlinePlaceholder: _13, langCode: _14, emojiStatus: _15, usernames: _16, storiesMaxId: _17, color: _18, profileColor: _19, botDailyUsers: _20)
|
||||
return Api.User.user(flags: _1!, flags2: _2!, id: _3!, accessHash: _4, firstName: _5, lastName: _6, username: _7, phone: _8, photo: _9, status: _10, botInfoVersion: _11, restrictionReason: _12, botInlinePlaceholder: _13, langCode: _14, emojiStatus: _15, usernames: _16, storiesMaxId: _17, color: _18, profileColor: _19, botActiveUsers: _20)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
|
@ -7448,6 +7448,26 @@ public extension Api.functions.messages {
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.messages {
|
||||
static func requestMainWebView(flags: Int32, peer: Api.InputPeer, bot: Api.InputUser, startParam: String?, themeParams: Api.DataJSON?, platform: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.WebViewResult>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-908059013)
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
peer.serialize(buffer, true)
|
||||
bot.serialize(buffer, true)
|
||||
if Int(flags) & Int(1 << 1) != 0 {serializeString(startParam!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 0) != 0 {themeParams!.serialize(buffer, true)}
|
||||
serializeString(platform, buffer: buffer, boxed: false)
|
||||
return (FunctionDescription(name: "messages.requestMainWebView", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("bot", String(describing: bot)), ("startParam", String(describing: startParam)), ("themeParams", String(describing: themeParams)), ("platform", String(describing: platform))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.WebViewResult? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.WebViewResult?
|
||||
if let signature = reader.readInt32() {
|
||||
result = Api.parse(reader, signature: signature) as? Api.WebViewResult
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
public extension Api.functions.messages {
|
||||
static func requestSimpleWebView(flags: Int32, bot: Api.InputUser, url: String?, startParam: String?, themeParams: Api.DataJSON?, platform: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.WebViewResult>) {
|
||||
let buffer = Buffer()
|
||||
|
@ -99,6 +99,9 @@ extension TelegramUser {
|
||||
if (flags2 & (1 << 11)) != 0 {
|
||||
botFlags.insert(.isBusiness)
|
||||
}
|
||||
if (flags2 & (1 << 13)) != 0 {
|
||||
botFlags.insert(.hasWebApp)
|
||||
}
|
||||
botInfo = BotUserInfo(flags: botFlags, inlinePlaceholder: botInlinePlaceholder)
|
||||
}
|
||||
|
||||
|
@ -38,6 +38,7 @@ public struct BotUserInfoFlags: OptionSet {
|
||||
public static let canBeAddedToAttachMenu = BotUserInfoFlags(rawValue: (1 << 4))
|
||||
public static let canEdit = BotUserInfoFlags(rawValue: (1 << 5))
|
||||
public static let isBusiness = BotUserInfoFlags(rawValue: (1 << 6))
|
||||
public static let hasWebApp = BotUserInfoFlags(rawValue: (1 << 7))
|
||||
}
|
||||
|
||||
public struct BotUserInfo: PostboxCoding, Equatable {
|
||||
|
@ -60,6 +60,50 @@ func _internal_requestSimpleWebView(postbox: Postbox, network: Network, botId: P
|
||||
|> switchToLatest
|
||||
}
|
||||
|
||||
func _internal_requestMainWebView(postbox: Postbox, network: Network, botId: PeerId, source: RequestSimpleWebViewSource, themeParams: [String: Any]?) -> Signal<RequestWebViewResult, RequestWebViewError> {
|
||||
var serializedThemeParams: Api.DataJSON?
|
||||
if let themeParams = themeParams, let data = try? JSONSerialization.data(withJSONObject: themeParams, options: []), let dataString = String(data: data, encoding: .utf8) {
|
||||
serializedThemeParams = .dataJSON(data: dataString)
|
||||
}
|
||||
return postbox.transaction { transaction -> Signal<RequestWebViewResult, RequestWebViewError> in
|
||||
guard let bot = transaction.getPeer(botId), let inputUser = apiInputUser(bot) else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
guard let peer = transaction.getPeer(botId), let inputPeer = apiInputPeer(peer) else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
|
||||
var flags: Int32 = 0
|
||||
if let _ = serializedThemeParams {
|
||||
flags |= (1 << 0)
|
||||
}
|
||||
switch source {
|
||||
case .inline:
|
||||
flags |= (1 << 1)
|
||||
case .settings:
|
||||
flags |= (1 << 2)
|
||||
default:
|
||||
break
|
||||
}
|
||||
return network.request(Api.functions.messages.requestMainWebView(flags: flags, peer: inputPeer, bot: inputUser, startParam: nil, themeParams: serializedThemeParams, platform: botWebViewPlatform))
|
||||
|> mapError { _ -> RequestWebViewError in
|
||||
return .generic
|
||||
}
|
||||
|> mapToSignal { result -> Signal<RequestWebViewResult, RequestWebViewError> in
|
||||
switch result {
|
||||
case let .webViewResultUrl(flags, queryId, url):
|
||||
var resultFlags: RequestWebViewResult.Flags = []
|
||||
if (flags & (1 << 1)) != 0 {
|
||||
resultFlags.insert(.fullSize)
|
||||
}
|
||||
return .single(RequestWebViewResult(flags: resultFlags, queryId: queryId, url: url, keepAliveSignal: nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
|> castError(RequestWebViewError.self)
|
||||
|> switchToLatest
|
||||
}
|
||||
|
||||
public enum KeepWebViewError {
|
||||
case generic
|
||||
}
|
||||
|
@ -1290,7 +1290,7 @@ func _internal_uploadBotPreviewImpl(
|
||||
}
|
||||
|
||||
let passFetchProgress = media is TelegramMediaFile
|
||||
let (contentSignal, _) = uploadedStoryContent(postbox: postbox, network: network, media: media, mediaReference: nil, embeddedStickers: embeddedStickers, accountPeerId: accountPeerId, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, auxiliaryMethods: auxiliaryMethods, passFetchProgress: passFetchProgress)
|
||||
let (contentSignal, originalMedia) = uploadedStoryContent(postbox: postbox, network: network, media: media, mediaReference: nil, embeddedStickers: embeddedStickers, accountPeerId: accountPeerId, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, auxiliaryMethods: auxiliaryMethods, passFetchProgress: passFetchProgress)
|
||||
return contentSignal
|
||||
|> mapToSignal { result -> Signal<StoryUploadResult, NoError> in
|
||||
switch result {
|
||||
@ -1319,6 +1319,8 @@ func _internal_uploadBotPreviewImpl(
|
||||
}
|
||||
|
||||
if let resultMediaValue = textMediaAndExpirationTimerFromApiMedia(resultMedia, toPeerId).media {
|
||||
applyMediaResourceChanges(from: originalMedia, to: resultMediaValue, postbox: postbox, force: originalMedia is TelegramMediaFile && resultMediaValue is TelegramMediaFile)
|
||||
|
||||
transaction.updatePeerCachedData(peerIds: Set([toPeerId]), update: { _, current in
|
||||
guard var current = current as? CachedUserData else {
|
||||
return current
|
||||
@ -1330,7 +1332,7 @@ func _internal_uploadBotPreviewImpl(
|
||||
if let index = media.firstIndex(where: { $0.id == resultMediaValue.id }) {
|
||||
media.remove(at: index)
|
||||
}
|
||||
media.append(resultMediaValue)
|
||||
media.insert(resultMediaValue, at: 0)
|
||||
let botPreview = CachedUserData.BotPreview(media: media)
|
||||
current = current.withUpdatedBotPreview(botPreview)
|
||||
return current
|
||||
|
@ -2087,13 +2087,15 @@ public final class BotPreviewStoryListContext: StoryListContext {
|
||||
|
||||
private var isLoadingMore: Bool = false
|
||||
private var requestDisposable: Disposable?
|
||||
|
||||
private var updatesDisposable: Disposable?
|
||||
private let reorderDisposable = MetaDisposable()
|
||||
|
||||
private var completionCallbacksByToken: [AnyHashable: [() -> Void]] = [:]
|
||||
|
||||
private var nextId: Int32 = 1
|
||||
private var pendingIdMapping: [Int32: Int32] = [:]
|
||||
private var idMapping: [MediaId: Int32] = [:]
|
||||
private var reverseIdMapping: [Int32: MediaId] = [:]
|
||||
|
||||
init(queue: Queue, account: Account, engine: TelegramEngine, peerId: EnginePeer.Id) {
|
||||
self.queue = queue
|
||||
@ -2107,17 +2109,68 @@ public final class BotPreviewStoryListContext: StoryListContext {
|
||||
|
||||
self.stateValue = State(peerReference: nil, items: [], pinnedIds: Set(), totalCount: 0, loadMoreToken: AnyHashable(0 as Int), isCached: true, hasCache: false, allEntityFiles: [:], isLoading: false)
|
||||
|
||||
self.requestDisposable = (engine.data.subscribe(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
|
||||
TelegramEngine.EngineData.Item.Peer.BotPreview(id: peerId)
|
||||
let localStateKey: PostboxViewKey = .storiesState(key: .local)
|
||||
|
||||
self.requestDisposable = (combineLatest(queue: queue,
|
||||
engine.data.subscribe(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
|
||||
TelegramEngine.EngineData.Item.Peer.BotPreview(id: peerId)
|
||||
),
|
||||
account.postbox.combinedView(keys: [localStateKey])
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer, botPreview in
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] peerAndBotPreview, combinedView in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let (peer, botPreview) = peerAndBotPreview
|
||||
|
||||
var items: [State.Item] = []
|
||||
|
||||
if let stateView = combinedView.views[localStateKey] as? StoryStatesView, let localState = stateView.value?.get(Stories.LocalState.self) {
|
||||
for item in localState.items.reversed() {
|
||||
let mappedId: Int32
|
||||
if let current = self.pendingIdMapping[item.stableId] {
|
||||
mappedId = current
|
||||
} else {
|
||||
mappedId = self.nextId
|
||||
self.nextId += 1
|
||||
self.pendingIdMapping[item.stableId] = mappedId
|
||||
}
|
||||
if case .botPreview(peerId) = item.target {
|
||||
items.append(State.Item(
|
||||
id: StoryId(peerId: peerId, id: mappedId),
|
||||
storyItem: EngineStoryItem(
|
||||
id: mappedId,
|
||||
timestamp: 0,
|
||||
expirationTimestamp: Int32.max,
|
||||
media: EngineMedia(item.media),
|
||||
alternativeMedia: nil,
|
||||
mediaAreas: [],
|
||||
text: "",
|
||||
entities: [],
|
||||
views: nil,
|
||||
privacy: nil,
|
||||
isPinned: false,
|
||||
isExpired: false,
|
||||
isPublic: false,
|
||||
isPending: true,
|
||||
isCloseFriends: false,
|
||||
isContacts: false,
|
||||
isSelectedContacts: false,
|
||||
isForwardingDisabled: false,
|
||||
isEdited: false,
|
||||
isMy: false,
|
||||
myReaction: nil,
|
||||
forwardInfo: nil,
|
||||
author: nil
|
||||
),
|
||||
peer: nil
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let botPreview {
|
||||
for media in botPreview.media {
|
||||
guard let mediaId = media.id else {
|
||||
@ -2131,6 +2184,7 @@ public final class BotPreviewStoryListContext: StoryListContext {
|
||||
id = self.nextId
|
||||
self.nextId += 1
|
||||
self.idMapping[mediaId] = id
|
||||
self.reverseIdMapping[id] = mediaId
|
||||
}
|
||||
|
||||
items.append(State.Item(
|
||||
@ -2181,10 +2235,77 @@ public final class BotPreviewStoryListContext: StoryListContext {
|
||||
|
||||
deinit {
|
||||
self.requestDisposable?.dispose()
|
||||
self.updatesDisposable?.dispose()
|
||||
self.reorderDisposable.dispose()
|
||||
}
|
||||
|
||||
func loadMore(completion: (() -> Void)?) {
|
||||
}
|
||||
|
||||
func reorderItems(ids: [StoryId]) {
|
||||
let peerId = self.peerId
|
||||
let idMapping = self.idMapping
|
||||
let reverseIdMapping = self.reverseIdMapping
|
||||
|
||||
let _ = (self.account.postbox.transaction({ transaction -> (Api.InputUser?, [Api.InputMedia]) in
|
||||
let inputUser = transaction.getPeer(peerId).flatMap(apiInputUser)
|
||||
|
||||
var inputMedia: [Api.InputMedia] = []
|
||||
transaction.updatePeerCachedData(peerIds: Set([self.peerId]), update: { _, current in
|
||||
guard var current = current as? CachedUserData else {
|
||||
return current
|
||||
}
|
||||
guard let currentBotPreview = current.botPreview else {
|
||||
return current
|
||||
}
|
||||
|
||||
var media: [Media] = []
|
||||
media = []
|
||||
|
||||
var seenIds = Set<Int32>()
|
||||
for id in ids {
|
||||
guard let mediaId = reverseIdMapping[id.id] else {
|
||||
continue
|
||||
}
|
||||
if let index = currentBotPreview.media.firstIndex(where: { $0.id == mediaId }) {
|
||||
seenIds.insert(id.id)
|
||||
media.append(currentBotPreview.media[index])
|
||||
}
|
||||
}
|
||||
|
||||
for item in currentBotPreview.media {
|
||||
guard let id = item.id, let storyId = idMapping[id] else {
|
||||
continue
|
||||
}
|
||||
if !seenIds.contains(storyId) {
|
||||
media.append(item)
|
||||
}
|
||||
}
|
||||
|
||||
for item in media {
|
||||
if let image = item as? TelegramMediaImage, let resource = image.representations.last?.resource as? CloudPhotoSizeMediaResource {
|
||||
inputMedia.append(.inputMediaPhoto(flags: 0, id: .inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil))
|
||||
inputMedia.append(Api.InputMedia.inputMediaPhoto(flags: 0, id: Api.InputPhoto.inputPhoto(id: resource.photoId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference)), ttlSeconds: nil))
|
||||
} else if let file = item as? TelegramMediaFile, let resource = file.resource as? CloudDocumentMediaResource {
|
||||
inputMedia.append(.inputMediaDocument(flags: 0, id: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: nil))
|
||||
}
|
||||
}
|
||||
|
||||
let botPreview = CachedUserData.BotPreview(media: media)
|
||||
current = current.withUpdatedBotPreview(botPreview)
|
||||
return current
|
||||
})
|
||||
|
||||
return (inputUser, inputMedia)
|
||||
})
|
||||
|> deliverOn(self.queue)).startStandalone(next: { [weak self] inputUser, inputMedia in
|
||||
guard let self, let inputUser else {
|
||||
return
|
||||
}
|
||||
let signal = self.account.network.request(Api.functions.bots.reorderPreviewMedias(bot: inputUser, order: inputMedia))
|
||||
self.reorderDisposable.set(signal.startStrict())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public var state: Signal<State, NoError> {
|
||||
@ -2206,7 +2327,13 @@ public final class BotPreviewStoryListContext: StoryListContext {
|
||||
|
||||
public func loadMore(completion: (() -> Void)? = nil) {
|
||||
self.impl.with { impl in
|
||||
impl.loadMore(completion : completion)
|
||||
impl.loadMore(completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
public func reorderItems(ids: [StoryId]) {
|
||||
self.impl.with { impl in
|
||||
impl.reorderItems(ids: ids)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -574,6 +574,10 @@ public extension TelegramEngine {
|
||||
return _internal_requestSimpleWebView(postbox: self.account.postbox, network: self.account.network, botId: botId, url: url, source: source, themeParams: themeParams)
|
||||
}
|
||||
|
||||
public func requestMainWebView(botId: PeerId, source: RequestSimpleWebViewSource, themeParams: [String: Any]?) -> Signal<RequestWebViewResult, RequestWebViewError> {
|
||||
return _internal_requestMainWebView(postbox: self.account.postbox, network: self.account.network, botId: botId, source: source, themeParams: themeParams)
|
||||
}
|
||||
|
||||
public func requestAppWebView(peerId: PeerId, appReference: BotAppReference, payload: String?, themeParams: [String: Any]?, compact: Bool, allowWrite: Bool) -> Signal<RequestWebViewResult, RequestWebViewError> {
|
||||
return _internal_requestAppWebView(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, peerId: peerId, appReference: appReference, payload: payload, themeParams: themeParams, compact: compact, allowWrite: allowWrite)
|
||||
}
|
||||
|
@ -306,3 +306,26 @@ public func _internal_managedUpdatedRecentApps(accountPeerId: PeerId, postbox: P
|
||||
return updateOnce
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_removeRecentlyUsedApp(account: Account, peerId: PeerId) -> Signal<Void, NoError> {
|
||||
return account.postbox.transaction { transaction -> Signal<Void, NoError> in
|
||||
if let entry = transaction.retrieveItemCacheEntry(id: cachedRecentAppsEntryId()), let recentPeers = entry.get(CachedRecentPeers.self) {
|
||||
let updatedRecentPeers = CachedRecentPeers(enabled: recentPeers.enabled, ids: recentPeers.ids.filter({ $0 != peerId }))
|
||||
if let updatedEntry = CodableEntry(updatedRecentPeers) {
|
||||
transaction.putItemCacheEntry(id: cachedRecentAppsEntryId(), entry: updatedEntry)
|
||||
}
|
||||
}
|
||||
|
||||
if let peer = transaction.getPeer(peerId), let apiPeer = apiInputPeer(peer) {
|
||||
return account.network.request(Api.functions.contacts.resetTopPeerRating(category: .topPeerCategoryBotsApp, peer: apiPeer))
|
||||
|> `catch` { _ -> Signal<Api.Bool, NoError> in
|
||||
return .single(.boolFalse)
|
||||
}
|
||||
|> mapToSignal { _ -> Signal<Void, NoError> in
|
||||
return .complete()
|
||||
}
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
} |> switchToLatest
|
||||
}
|
||||
|
@ -633,6 +633,10 @@ public extension TelegramEngine {
|
||||
public func removeRecentlyUsedInlineBot(peerId: PeerId) -> Signal<Void, NoError> {
|
||||
return _internal_removeRecentlyUsedInlineBot(account: self.account, peerId: peerId)
|
||||
}
|
||||
|
||||
public func removeRecentlyUsedApp(peerId: PeerId) -> Signal<Void, NoError> {
|
||||
return _internal_removeRecentlyUsedApp(account: self.account, peerId: peerId)
|
||||
}
|
||||
|
||||
public func uploadedPeerPhoto(resource: MediaResource) -> Signal<UploadedPeerPhotoData, NoError> {
|
||||
return _internal_uploadedPeerPhoto(postbox: self.account.postbox, network: self.account.network, resource: resource)
|
||||
|
@ -457,6 +457,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/PeerManagement/OldChannelsController",
|
||||
"//submodules/TelegramUI/Components/Chat/ChatSendStarsScreen",
|
||||
"//submodules/TelegramUI/Components/MinimizedContainer",
|
||||
"//submodules/TelegramUI/Components/SpaceWarpView",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
"//build-system:ios_sim_arm64": [],
|
||||
|
@ -1396,15 +1396,18 @@ public class CameraScreen: ViewController {
|
||||
public weak var destinationView: UIView?
|
||||
public let destinationRect: CGRect
|
||||
public let destinationCornerRadius: CGFloat
|
||||
public let completion: (() -> Void)?
|
||||
|
||||
public init(
|
||||
destinationView: UIView,
|
||||
destinationRect: CGRect,
|
||||
destinationCornerRadius: CGFloat
|
||||
destinationCornerRadius: CGFloat,
|
||||
completion: (() -> Void)? = nil
|
||||
) {
|
||||
self.destinationView = destinationView
|
||||
self.destinationRect = destinationRect
|
||||
self.destinationCornerRadius = destinationCornerRadius
|
||||
self.completion = completion
|
||||
}
|
||||
}
|
||||
|
||||
@ -2182,9 +2185,12 @@ public class CameraScreen: ViewController {
|
||||
let destinationLocalFrame = destinationView.convert(transitionOut.destinationRect, to: self.view)
|
||||
let targetScale = destinationLocalFrame.width / self.previewContainerView.frame.width
|
||||
|
||||
let transitionOutCompletion = transitionOut.completion
|
||||
|
||||
if case .story = controller.mode {
|
||||
self.previewContainerView.layer.animatePosition(from: self.previewContainerView.center, to: destinationLocalFrame.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
|
||||
completion()
|
||||
transitionOutCompletion?()
|
||||
})
|
||||
self.previewContainerView.layer.animateScale(from: 1.0, to: targetScale, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
|
||||
@ -2208,6 +2214,7 @@ public class CameraScreen: ViewController {
|
||||
self.mainPreviewAnimationWrapperView.center = destinationInnerFrame.center
|
||||
self.mainPreviewAnimationWrapperView.layer.animatePosition(from: initialCenter, to: self.mainPreviewAnimationWrapperView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
|
||||
completion()
|
||||
transitionOutCompletion?()
|
||||
})
|
||||
|
||||
var targetBounds = self.mainPreviewView.bounds
|
||||
|
@ -2426,15 +2426,18 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
public weak var destinationView: UIView?
|
||||
public let destinationRect: CGRect
|
||||
public let destinationCornerRadius: CGFloat
|
||||
public let completion: (() -> Void)?
|
||||
|
||||
public init(
|
||||
destinationView: UIView,
|
||||
destinationRect: CGRect,
|
||||
destinationCornerRadius: CGFloat
|
||||
destinationCornerRadius: CGFloat,
|
||||
completion: (() -> Void)? = nil
|
||||
) {
|
||||
self.destinationView = destinationView
|
||||
self.destinationRect = destinationRect
|
||||
self.destinationCornerRadius = destinationCornerRadius
|
||||
self.completion = completion
|
||||
}
|
||||
}
|
||||
|
||||
@ -3769,6 +3772,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
if let transitionOut = controller.transitionOut(finished, isNew), let destinationView = transitionOut.destinationView {
|
||||
var destinationTransitionView: UIView?
|
||||
var destinationTransitionRect: CGRect = .zero
|
||||
let transitionOutCompletion = transitionOut.completion
|
||||
if !finished {
|
||||
if let transitionIn = controller.transitionIn, case let .gallery(galleryTransitionIn) = transitionIn, let sourceImage = galleryTransitionIn.sourceImage, isNew != true {
|
||||
let sourceSuperView = galleryTransitionIn.sourceView?.superview?.superview
|
||||
@ -3851,6 +3855,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
||||
destinationView.isHidden = false
|
||||
destinationSnapshotView?.removeFromSuperview()
|
||||
completion()
|
||||
transitionOutCompletion?()
|
||||
})
|
||||
self.previewContainerView.layer.animateScale(from: 1.0, to: destinationScale, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
self.previewContainerView.layer.animateBounds(from: self.previewContainerView.bounds, to: CGRect(origin: CGPoint(x: 0.0, y: (self.previewContainerView.bounds.height - self.previewContainerView.bounds.width * destinationAspectRatio) / 2.0), size: CGSize(width: self.previewContainerView.bounds.width, height: self.previewContainerView.bounds.width * destinationAspectRatio)), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
|
@ -673,7 +673,11 @@ private final class PeerInfoScreenLabeledValueItemNode: PeerInfoScreenItemNode {
|
||||
}
|
||||
|
||||
if let button = item.button {
|
||||
height += 3.0
|
||||
if textSize.height > 0.0 {
|
||||
height += 3.0
|
||||
} else {
|
||||
height -= 7.0
|
||||
}
|
||||
|
||||
let actionButton: ComponentView<Empty>
|
||||
if let current = self.actionButton {
|
||||
|
@ -30,6 +30,7 @@ final class PeerInfoState {
|
||||
let isEditing: Bool
|
||||
let selectedMessageIds: Set<MessageId>?
|
||||
let selectedStoryIds: Set<Int32>?
|
||||
let paneIsReordering: Bool
|
||||
let updatingAvatar: PeerInfoUpdatingAvatar?
|
||||
let updatingBio: String?
|
||||
let avatarUploadProgress: AvatarUploadProgress?
|
||||
@ -42,6 +43,7 @@ final class PeerInfoState {
|
||||
isEditing: Bool,
|
||||
selectedMessageIds: Set<MessageId>?,
|
||||
selectedStoryIds: Set<Int32>?,
|
||||
paneIsReordering: Bool,
|
||||
updatingAvatar: PeerInfoUpdatingAvatar?,
|
||||
updatingBio: String?,
|
||||
avatarUploadProgress: AvatarUploadProgress?,
|
||||
@ -53,6 +55,7 @@ final class PeerInfoState {
|
||||
self.isEditing = isEditing
|
||||
self.selectedMessageIds = selectedMessageIds
|
||||
self.selectedStoryIds = selectedStoryIds
|
||||
self.paneIsReordering = paneIsReordering
|
||||
self.updatingAvatar = updatingAvatar
|
||||
self.updatingBio = updatingBio
|
||||
self.avatarUploadProgress = avatarUploadProgress
|
||||
@ -67,6 +70,7 @@ final class PeerInfoState {
|
||||
isEditing: isEditing,
|
||||
selectedMessageIds: self.selectedMessageIds,
|
||||
selectedStoryIds: self.selectedStoryIds,
|
||||
paneIsReordering: self.paneIsReordering,
|
||||
updatingAvatar: self.updatingAvatar,
|
||||
updatingBio: self.updatingBio,
|
||||
avatarUploadProgress: self.avatarUploadProgress,
|
||||
@ -82,6 +86,7 @@ final class PeerInfoState {
|
||||
isEditing: self.isEditing,
|
||||
selectedMessageIds: selectedMessageIds,
|
||||
selectedStoryIds: self.selectedStoryIds,
|
||||
paneIsReordering: self.paneIsReordering,
|
||||
updatingAvatar: self.updatingAvatar,
|
||||
updatingBio: self.updatingBio,
|
||||
avatarUploadProgress: self.avatarUploadProgress,
|
||||
@ -97,6 +102,23 @@ final class PeerInfoState {
|
||||
isEditing: self.isEditing,
|
||||
selectedMessageIds: self.selectedMessageIds,
|
||||
selectedStoryIds: selectedStoryIds,
|
||||
paneIsReordering: self.paneIsReordering,
|
||||
updatingAvatar: self.updatingAvatar,
|
||||
updatingBio: self.updatingBio,
|
||||
avatarUploadProgress: self.avatarUploadProgress,
|
||||
highlightedButton: self.highlightedButton,
|
||||
isEditingBirthDate: self.isEditingBirthDate,
|
||||
updatingBirthDate: self.updatingBirthDate,
|
||||
personalChannels: self.personalChannels
|
||||
)
|
||||
}
|
||||
|
||||
func withPaneIsReordering(_ paneIsReordering: Bool) -> PeerInfoState {
|
||||
return PeerInfoState(
|
||||
isEditing: self.isEditing,
|
||||
selectedMessageIds: self.selectedMessageIds,
|
||||
selectedStoryIds: self.selectedStoryIds,
|
||||
paneIsReordering: paneIsReordering,
|
||||
updatingAvatar: self.updatingAvatar,
|
||||
updatingBio: self.updatingBio,
|
||||
avatarUploadProgress: self.avatarUploadProgress,
|
||||
@ -112,6 +134,7 @@ final class PeerInfoState {
|
||||
isEditing: self.isEditing,
|
||||
selectedMessageIds: self.selectedMessageIds,
|
||||
selectedStoryIds: self.selectedStoryIds,
|
||||
paneIsReordering: self.paneIsReordering,
|
||||
updatingAvatar: updatingAvatar,
|
||||
updatingBio: self.updatingBio,
|
||||
avatarUploadProgress: self.avatarUploadProgress,
|
||||
@ -127,6 +150,7 @@ final class PeerInfoState {
|
||||
isEditing: self.isEditing,
|
||||
selectedMessageIds: self.selectedMessageIds,
|
||||
selectedStoryIds: self.selectedStoryIds,
|
||||
paneIsReordering: self.paneIsReordering,
|
||||
updatingAvatar: self.updatingAvatar,
|
||||
updatingBio: updatingBio,
|
||||
avatarUploadProgress: self.avatarUploadProgress,
|
||||
@ -142,6 +166,7 @@ final class PeerInfoState {
|
||||
isEditing: self.isEditing,
|
||||
selectedMessageIds: self.selectedMessageIds,
|
||||
selectedStoryIds: self.selectedStoryIds,
|
||||
paneIsReordering: self.paneIsReordering,
|
||||
updatingAvatar: self.updatingAvatar,
|
||||
updatingBio: self.updatingBio,
|
||||
avatarUploadProgress: avatarUploadProgress,
|
||||
@ -157,6 +182,7 @@ final class PeerInfoState {
|
||||
isEditing: self.isEditing,
|
||||
selectedMessageIds: self.selectedMessageIds,
|
||||
selectedStoryIds: self.selectedStoryIds,
|
||||
paneIsReordering: self.paneIsReordering,
|
||||
updatingAvatar: self.updatingAvatar,
|
||||
updatingBio: self.updatingBio,
|
||||
avatarUploadProgress: self.avatarUploadProgress,
|
||||
@ -172,6 +198,7 @@ final class PeerInfoState {
|
||||
isEditing: self.isEditing,
|
||||
selectedMessageIds: self.selectedMessageIds,
|
||||
selectedStoryIds: self.selectedStoryIds,
|
||||
paneIsReordering: self.paneIsReordering,
|
||||
updatingAvatar: self.updatingAvatar,
|
||||
updatingBio: self.updatingBio,
|
||||
avatarUploadProgress: self.avatarUploadProgress,
|
||||
@ -187,6 +214,7 @@ final class PeerInfoState {
|
||||
isEditing: self.isEditing,
|
||||
selectedMessageIds: self.selectedMessageIds,
|
||||
selectedStoryIds: self.selectedStoryIds,
|
||||
paneIsReordering: self.paneIsReordering,
|
||||
updatingAvatar: self.updatingAvatar,
|
||||
updatingBio: self.updatingBio,
|
||||
avatarUploadProgress: self.avatarUploadProgress,
|
||||
@ -202,6 +230,7 @@ final class PeerInfoState {
|
||||
isEditing: self.isEditing,
|
||||
selectedMessageIds: self.selectedMessageIds,
|
||||
selectedStoryIds: self.selectedStoryIds,
|
||||
paneIsReordering: self.paneIsReordering,
|
||||
updatingAvatar: self.updatingAvatar,
|
||||
updatingBio: self.updatingBio,
|
||||
avatarUploadProgress: self.avatarUploadProgress,
|
||||
|
@ -810,7 +810,20 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat
|
||||
}
|
||||
for (_, pane) in self.pendingPanes {
|
||||
if let paneNode = pane.pane.node as? PeerInfoStoryPaneNode {
|
||||
paneNode.updateSelectedStories(selectedStoryIds: selectedStoryIds, animated: animated)
|
||||
paneNode.updateSelectedStories(selectedStoryIds: selectedStoryIds, animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updatePaneIsReordering(isReordering: Bool, animated: Bool) {
|
||||
for (_, pane) in self.currentPanes {
|
||||
if let paneNode = pane.node as? PeerInfoStoryPaneNode {
|
||||
paneNode.updateIsReordering(isReordering: isReordering, animated: animated)
|
||||
}
|
||||
}
|
||||
for (_, pane) in self.pendingPanes {
|
||||
if let paneNode = pane.pane.node as? PeerInfoStoryPaneNode {
|
||||
paneNode.updateIsReordering(isReordering: isReordering, animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1336,6 +1336,16 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
|
||||
}))
|
||||
}
|
||||
|
||||
var hasAbout = false
|
||||
if let about = cachedData.about, !about.isEmpty {
|
||||
hasAbout = true
|
||||
}
|
||||
|
||||
var hasWebApp = false
|
||||
if let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp) {
|
||||
hasWebApp = true
|
||||
}
|
||||
|
||||
if user.isFake {
|
||||
items[currentPeerInfoSection]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: "", text: user.botInfo != nil ? presentationData.strings.UserInfo_FakeBotWarning : presentationData.strings.UserInfo_FakeUserWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.botInfo != nil ? enabledPrivateBioEntities : []), action: nil, requestLayout: {
|
||||
interaction.requestLayout(false)
|
||||
@ -1344,28 +1354,36 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
|
||||
items[currentPeerInfoSection]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo, text: user.botInfo != nil ? presentationData.strings.UserInfo_ScamBotWarning : presentationData.strings.UserInfo_ScamUserWarning, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.botInfo != nil ? enabledPrivateBioEntities : []), action: nil, requestLayout: {
|
||||
interaction.requestLayout(false)
|
||||
}))
|
||||
} else if let about = cachedData.about, !about.isEmpty {
|
||||
} else if hasAbout || hasWebApp {
|
||||
var actionButton: PeerInfoScreenLabeledValueItem.Button?
|
||||
if let menuButton = cachedData.botInfo?.menuButton, case let .webView(text, url) = menuButton {
|
||||
if hasWebApp {
|
||||
//TODO:localize
|
||||
actionButton = PeerInfoScreenLabeledValueItem.Button(title: "Open App", action: {
|
||||
guard let parentController = interaction.getController() else {
|
||||
return
|
||||
}
|
||||
|
||||
openWebApp(
|
||||
parentController: parentController,
|
||||
context.sharedContext.openWebApp(
|
||||
context: context,
|
||||
parentController: parentController,
|
||||
updatedPresentationData: nil,
|
||||
peer: .user(user),
|
||||
buttonText: text,
|
||||
url: url,
|
||||
simple: false,
|
||||
source: .menu
|
||||
threadId: nil,
|
||||
buttonText: "",
|
||||
url: "",
|
||||
simple: true,
|
||||
source: .generic,
|
||||
skipTermsOfService: true
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
items[currentPeerInfoSection]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo, text: about, textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.isPremium ? enabledPublicBioEntities : enabledPrivateBioEntities), action: isMyProfile ? { node, _ in
|
||||
var label: String = ""
|
||||
if let about = cachedData.about, !about.isEmpty {
|
||||
label = user.botInfo == nil ? presentationData.strings.Profile_About : presentationData.strings.Profile_BotInfo
|
||||
}
|
||||
|
||||
items[currentPeerInfoSection]!.append(PeerInfoScreenLabeledValueItem(id: 0, label: label, text: cachedData.about ?? "", textColor: .primary, textBehavior: .multiLine(maxLines: 100, enabledEntities: user.isPremium ? enabledPublicBioEntities : enabledPrivateBioEntities), action: isMyProfile ? { node, _ in
|
||||
bioContextAction(node, nil, nil)
|
||||
} : nil, linkItemAction: bioLinkAction, button: actionButton, contextAction: bioContextAction, requestLayout: {
|
||||
interaction.requestLayout(false)
|
||||
@ -2569,6 +2587,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
isEditing: false,
|
||||
selectedMessageIds: nil,
|
||||
selectedStoryIds: nil,
|
||||
paneIsReordering: false,
|
||||
updatingAvatar: nil,
|
||||
updatingBio: nil,
|
||||
avatarUploadProgress: nil,
|
||||
@ -4220,13 +4239,14 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
strongSelf.chatInterfaceInteraction.selectionState = strongSelf.state.selectedMessageIds.flatMap { ChatInterfaceSelectionState(selectedIds: $0) }
|
||||
strongSelf.paneContainerNode.updateSelectedMessageIds(strongSelf.state.selectedMessageIds, animated: true)
|
||||
case .selectionDone:
|
||||
strongSelf.state = strongSelf.state.withSelectedMessageIds(nil).withSelectedStoryIds(nil)
|
||||
strongSelf.state = strongSelf.state.withSelectedMessageIds(nil).withSelectedStoryIds(nil).withPaneIsReordering(false)
|
||||
if let (layout, navigationHeight) = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring), additive: false)
|
||||
}
|
||||
strongSelf.chatInterfaceInteraction.selectionState = strongSelf.state.selectedMessageIds.flatMap { ChatInterfaceSelectionState(selectedIds: $0) }
|
||||
strongSelf.paneContainerNode.updateSelectedMessageIds(strongSelf.state.selectedMessageIds, animated: true)
|
||||
strongSelf.paneContainerNode.updateSelectedStoryIds(strongSelf.state.selectedStoryIds, animated: true)
|
||||
strongSelf.paneContainerNode.updatePaneIsReordering(isReordering: strongSelf.state.paneIsReordering, animated: true)
|
||||
case .search, .searchWithTags, .standaloneSearch:
|
||||
strongSelf.activateSearch()
|
||||
case .more:
|
||||
@ -9924,16 +9944,31 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
return nil
|
||||
}
|
||||
|
||||
if !self.headerNode.isAvatarExpanded {
|
||||
let transitionView = self.headerNode.avatarListNode.avatarContainerNode.avatarNode.contentNode.view
|
||||
return StoryCameraTransitionOut(
|
||||
destinationView: transitionView,
|
||||
destinationRect: transitionView.bounds,
|
||||
destinationCornerRadius: transitionView.bounds.height * 0.5
|
||||
)
|
||||
if let data = self.data, let user = data.peer as? TelegramUser, let _ = user.botInfo {
|
||||
if let pane = self.paneContainerNode.currentPane?.node as? PeerInfoStoryPaneNode, let transitionView = pane.extractPendingStoryTransitionView() {
|
||||
return StoryCameraTransitionOut(
|
||||
destinationView: transitionView,
|
||||
destinationRect: transitionView.bounds,
|
||||
destinationCornerRadius: 0.0,
|
||||
completion: { [weak transitionView] in
|
||||
transitionView?.removeFromSuperview()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
} else {
|
||||
if !self.headerNode.isAvatarExpanded {
|
||||
let transitionView = self.headerNode.avatarListNode.avatarContainerNode.avatarNode.contentNode.view
|
||||
return StoryCameraTransitionOut(
|
||||
destinationView: transitionView,
|
||||
destinationRect: transitionView.bounds,
|
||||
destinationCornerRadius: transitionView.bounds.height * 0.5
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -10860,18 +10895,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, a in
|
||||
if ignoreNextActions {
|
||||
return
|
||||
}
|
||||
ignoreNextActions = true
|
||||
a(.default)
|
||||
|
||||
let _ = self
|
||||
})))
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: "Select", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak pane] _, a in
|
||||
if ignoreNextActions {
|
||||
return
|
||||
@ -10880,7 +10903,21 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
a(.default)
|
||||
|
||||
if let pane {
|
||||
pane.setIsSelectionModeActive(true)
|
||||
pane.beginReordering()
|
||||
}
|
||||
})))
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: "Select", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { [weak self] _, a in
|
||||
if ignoreNextActions {
|
||||
return
|
||||
}
|
||||
ignoreNextActions = true
|
||||
a(.default)
|
||||
|
||||
if let self {
|
||||
self.toggleStorySelection(ids: [], isSelected: true)
|
||||
}
|
||||
})))
|
||||
|
||||
@ -11289,7 +11326,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
self.headerNode.customNavigationContentNode = self.paneContainerNode.currentPane?.node.navigationContentNode
|
||||
|
||||
var isScrollEnabled = !self.isMediaOnly && self.headerNode.customNavigationContentNode == nil
|
||||
if self.state.selectedStoryIds != nil {
|
||||
if self.state.selectedStoryIds != nil || self.state.paneIsReordering {
|
||||
isScrollEnabled = false
|
||||
}
|
||||
if self.scrollNode.view.isScrollEnabled != isScrollEnabled {
|
||||
@ -11724,7 +11761,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
}
|
||||
|
||||
var disableTabSwitching = false
|
||||
if self.state.selectedStoryIds != nil {
|
||||
if self.state.selectedStoryIds != nil || self.state.paneIsReordering {
|
||||
disableTabSwitching = true
|
||||
}
|
||||
|
||||
@ -11755,7 +11792,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
rightNavigationButtons.insert(PeerInfoHeaderNavigationButtonSpec(key: .postStory, isForExpandedView: false), at: 0)
|
||||
}
|
||||
|
||||
if self.state.selectedMessageIds == nil && self.state.selectedStoryIds == nil {
|
||||
if self.state.selectedMessageIds == nil && self.state.selectedStoryIds == nil && !self.state.paneIsReordering {
|
||||
if let currentPaneKey = self.paneContainerNode.currentPaneKey {
|
||||
switch currentPaneKey {
|
||||
case .files, .music, .links, .members:
|
||||
@ -12120,6 +12157,17 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
self.paneContainerNode.updateSelectedStoryIds(self.state.selectedStoryIds, animated: true)
|
||||
}
|
||||
|
||||
func togglePaneIsReordering(isReordering: Bool) {
|
||||
self.expandTabs(animated: true)
|
||||
|
||||
self.state = self.state.withPaneIsReordering(true)
|
||||
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring), additive: false)
|
||||
}
|
||||
self.paneContainerNode.updatePaneIsReordering(isReordering: self.state.paneIsReordering, animated: true)
|
||||
}
|
||||
|
||||
func cancelItemSelection() {
|
||||
self.headerNode.navigationButtonContainer.performAction?(.selectionDone, nil, nil)
|
||||
}
|
||||
@ -12965,6 +13013,10 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
|
||||
self.controllerNode.toggleStorySelection(ids: ids, isSelected: isSelected)
|
||||
}
|
||||
|
||||
public func togglePaneIsReordering(isReordering: Bool) {
|
||||
self.controllerNode.togglePaneIsReordering(isReordering: isReordering)
|
||||
}
|
||||
|
||||
public func cancelItemSelection() {
|
||||
self.controllerNode.cancelItemSelection()
|
||||
}
|
||||
@ -13936,7 +13988,7 @@ private final class HeaderContextReferenceContentSource: ContextReferenceContent
|
||||
}
|
||||
}
|
||||
|
||||
private func openWebApp(parentController: ViewController, context: AccountContext, peer: EnginePeer, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource) {
|
||||
/*private func openWebApp(parentController: ViewController, context: AccountContext, peer: EnginePeer, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource) {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
|
||||
|
||||
let botName: String
|
||||
@ -14123,4 +14175,4 @@ private func openWebApp(parentController: ViewController, context: AccountContex
|
||||
parentController.present(controller, in: .window(.root))
|
||||
}
|
||||
})
|
||||
}
|
||||
}*/
|
||||
|
@ -553,6 +553,10 @@ private final class ItemLayer: CALayer, SparseItemGridLayer {
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override init(layer: Any) {
|
||||
super.init(layer: layer)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disposable?.dispose()
|
||||
@ -1104,6 +1108,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding {
|
||||
var onBeginFastScrollingImpl: (() -> Void)?
|
||||
var getShimmerColorsImpl: (() -> SparseItemGrid.ShimmerColors)?
|
||||
var updateShimmerLayersImpl: ((SparseItemGridDisplayItem) -> Void)?
|
||||
var reorderIfPossibleImpl: ((SparseItemGrid.Item, Int) -> Void)?
|
||||
|
||||
var revealedSpoilerMessageIds = Set<MessageId>()
|
||||
|
||||
@ -1356,6 +1361,12 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding {
|
||||
return .never()
|
||||
}
|
||||
}
|
||||
|
||||
func reorderIfPossible(item: SparseItemGrid.Item, toIndex: Int) {
|
||||
if let reorderIfPossibleImpl = self.reorderIfPossibleImpl {
|
||||
reorderIfPossibleImpl(item, toIndex)
|
||||
}
|
||||
}
|
||||
|
||||
func onTap(item: SparseItemGrid.Item, itemLayer: CALayer, point: CGPoint) {
|
||||
guard let item = item as? VisualMediaItem else {
|
||||
@ -1556,6 +1567,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
private let directMediaImageCache: DirectMediaImageCache
|
||||
private var items: SparseItemGrid.Items?
|
||||
private var pinnedIds: Set<Int32> = Set()
|
||||
private var reorderedIds: [StoryId]?
|
||||
private var itemCount: Int?
|
||||
private var didUpdateItemsOnce: Bool = false
|
||||
|
||||
@ -1576,6 +1588,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
return self.selectedIdsPromise.get()
|
||||
}
|
||||
|
||||
private var isReordering: Bool = false
|
||||
|
||||
public var selectedItems: [Int32: EngineStoryItem] {
|
||||
var result: [Int32: EngineStoryItem] = [:]
|
||||
for id in self.selectedIds {
|
||||
@ -1621,7 +1635,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
public var tabBarOffset: CGFloat {
|
||||
return self.itemGrid.coveringInsetOffset
|
||||
}
|
||||
|
||||
|
||||
private var currentListState: StoryListContext.State?
|
||||
private var listDisposable: Disposable?
|
||||
private var hiddenMediaDisposable: Disposable?
|
||||
private let updateDisposable = MetaDisposable()
|
||||
@ -1868,6 +1883,12 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
return nil
|
||||
}
|
||||
)
|
||||
storyContainerScreen.performReorderAction = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.beginReordering()
|
||||
}
|
||||
|
||||
self.hiddenMediaDisposable?.dispose()
|
||||
self.hiddenMediaDisposable = (storyContainerScreen.focusedItem
|
||||
@ -1913,6 +1934,13 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
}
|
||||
strongSelf.openCurrentDate?()
|
||||
}
|
||||
|
||||
self.itemGridBinding.reorderIfPossibleImpl = { [weak self] item, toIndex in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.reorderIfPossible(item: item, toIndex: toIndex)
|
||||
}
|
||||
|
||||
self.itemGridBinding.didScrollImpl = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
@ -2477,6 +2505,30 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
})))
|
||||
}
|
||||
|
||||
if canManage, case .botPreview = self.scope {
|
||||
//TODO:localize
|
||||
items.append(.action(ContextMenuActionItem(text: "Reorder", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in
|
||||
c?.dismiss(completion: {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.beginReordering()
|
||||
})
|
||||
})))
|
||||
|
||||
//TODO:localize
|
||||
items.append(.action(ContextMenuActionItem(text: "Edit Preview", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in
|
||||
c?.dismiss(completion: {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = self
|
||||
})
|
||||
})))
|
||||
}
|
||||
|
||||
if !item.isForwardingDisabled, case .everyone = item.privacy?.base {
|
||||
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.StoryList_ItemAction_Forward, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in
|
||||
c?.dismiss(completion: {
|
||||
@ -2621,7 +2673,6 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
self.listDisposable?.dispose()
|
||||
self.listDisposable = nil
|
||||
|
||||
let context = self.context
|
||||
self.listDisposable = (state
|
||||
|> deliverOn(queue)).startStrict(next: { [weak self] state in
|
||||
guard let self else {
|
||||
@ -2664,76 +2715,104 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
paneKey = .stories
|
||||
}
|
||||
self.statusPromise.set(.single(PeerInfoStatusData(text: title, isActivity: false, key: paneKey)))
|
||||
|
||||
let timezoneOffset = Int32(TimeZone.current.secondsFromGMT())
|
||||
|
||||
var mappedItems: [SparseItemGrid.Item] = []
|
||||
var mappedHoles: [SparseItemGrid.HoleAnchor] = []
|
||||
var totalCount: Int = 0
|
||||
for item in state.items {
|
||||
var peerReference: PeerReference?
|
||||
if let value = state.peerReference {
|
||||
peerReference = value
|
||||
} else if let peer = item.peer {
|
||||
peerReference = PeerReference(peer._asPeer())
|
||||
}
|
||||
guard let peerReference else {
|
||||
continue
|
||||
}
|
||||
|
||||
mappedItems.append(VisualMediaItem(
|
||||
index: mappedItems.count,
|
||||
peer: peerReference,
|
||||
storyId: item.id,
|
||||
story: item.storyItem,
|
||||
authorPeer: item.peer,
|
||||
isPinned: state.pinnedIds.contains(item.storyItem.id),
|
||||
localMonthTimestamp: Month(localTimestamp: item.storyItem.timestamp + timezoneOffset).packedValue
|
||||
))
|
||||
}
|
||||
if mappedItems.count < state.totalCount, let lastItem = state.items.last, let _ = state.loadMoreToken {
|
||||
mappedHoles.append(VisualMediaHoleAnchor(index: mappedItems.count, storyId: StoryId(peerId: context.account.peerId, id: Int32.max), localMonthTimestamp: Month(localTimestamp: lastItem.storyItem.timestamp + timezoneOffset).packedValue))
|
||||
}
|
||||
totalCount = state.totalCount
|
||||
totalCount = max(mappedItems.count, totalCount)
|
||||
|
||||
if totalCount == 0 && state.loadMoreToken != nil && !state.isCached {
|
||||
totalCount = 100
|
||||
}
|
||||
|
||||
Queue.mainQueue().async { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
var headerText: String?
|
||||
if case let .peer(peerId, _, isArchived) = strongSelf.scope {
|
||||
if isArchived && !mappedItems.isEmpty && peerId == strongSelf.context.account.peerId {
|
||||
headerText = strongSelf.presentationData.strings.StoryList_ArchiveDescription
|
||||
}
|
||||
}
|
||||
|
||||
let items = SparseItemGrid.Items(
|
||||
items: mappedItems,
|
||||
holeAnchors: mappedHoles,
|
||||
count: totalCount,
|
||||
itemBinding: strongSelf.itemGridBinding,
|
||||
headerText: headerText,
|
||||
snapTopInset: false
|
||||
)
|
||||
self.currentListState = state
|
||||
|
||||
strongSelf.itemCount = state.totalCount
|
||||
|
||||
let currentSynchronous = synchronous && firstTime
|
||||
let currentReloadAtTop = reloadAtTop && firstTime
|
||||
self.updateItemsFromState(state: state, firstTime: firstTime, reloadAtTop: reloadAtTop, synchronous: synchronous, animated: false)
|
||||
firstTime = false
|
||||
strongSelf.updateHistory(items: items, pinnedIds: state.pinnedIds, synchronous: currentSynchronous, reloadAtTop: currentReloadAtTop)
|
||||
strongSelf.isRequestingView = false
|
||||
self.isRequestingView = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func updateHistory(items: SparseItemGrid.Items, pinnedIds: Set<Int32>, synchronous: Bool, reloadAtTop: Bool) {
|
||||
private func updateItemsFromState(state: StoryListContext.State, firstTime: Bool, reloadAtTop: Bool, synchronous: Bool, animated: Bool) {
|
||||
let timezoneOffset = Int32(TimeZone.current.secondsFromGMT())
|
||||
|
||||
var mappedItems: [SparseItemGrid.Item] = []
|
||||
var mappedHoles: [SparseItemGrid.HoleAnchor] = []
|
||||
var totalCount: Int = 0
|
||||
|
||||
var stateItems = state.items
|
||||
if let reorderedIds = self.reorderedIds {
|
||||
var fixedStateItems: [StoryListContext.State.Item] = []
|
||||
|
||||
var seenIds = Set<StoryId>()
|
||||
for id in reorderedIds {
|
||||
if let index = stateItems.firstIndex(where: { $0.id == id }) {
|
||||
seenIds.insert(id)
|
||||
fixedStateItems.append(stateItems[index])
|
||||
}
|
||||
}
|
||||
|
||||
for item in stateItems {
|
||||
if !seenIds.contains(item.id) {
|
||||
fixedStateItems.append(item)
|
||||
}
|
||||
}
|
||||
stateItems = fixedStateItems
|
||||
self.reorderedIds = fixedStateItems.map(\.id)
|
||||
}
|
||||
|
||||
for item in stateItems {
|
||||
var peerReference: PeerReference?
|
||||
if let value = state.peerReference {
|
||||
peerReference = value
|
||||
} else if let peer = item.peer {
|
||||
peerReference = PeerReference(peer._asPeer())
|
||||
}
|
||||
guard let peerReference else {
|
||||
continue
|
||||
}
|
||||
|
||||
mappedItems.append(VisualMediaItem(
|
||||
index: mappedItems.count,
|
||||
peer: peerReference,
|
||||
storyId: item.id,
|
||||
story: item.storyItem,
|
||||
authorPeer: item.peer,
|
||||
isPinned: state.pinnedIds.contains(item.storyItem.id),
|
||||
localMonthTimestamp: Month(localTimestamp: item.storyItem.timestamp + timezoneOffset).packedValue
|
||||
))
|
||||
}
|
||||
if mappedItems.count < state.totalCount, let lastItem = state.items.last, let _ = state.loadMoreToken {
|
||||
mappedHoles.append(VisualMediaHoleAnchor(index: mappedItems.count, storyId: StoryId(peerId: context.account.peerId, id: Int32.max), localMonthTimestamp: Month(localTimestamp: lastItem.storyItem.timestamp + timezoneOffset).packedValue))
|
||||
}
|
||||
totalCount = state.totalCount
|
||||
totalCount = max(mappedItems.count, totalCount)
|
||||
|
||||
if totalCount == 0 && state.loadMoreToken != nil && !state.isCached {
|
||||
totalCount = 100
|
||||
}
|
||||
|
||||
var headerText: String?
|
||||
if case let .peer(peerId, _, isArchived) = self.scope {
|
||||
if isArchived && !mappedItems.isEmpty && peerId == self.context.account.peerId {
|
||||
headerText = self.presentationData.strings.StoryList_ArchiveDescription
|
||||
}
|
||||
}
|
||||
|
||||
let items = SparseItemGrid.Items(
|
||||
items: mappedItems,
|
||||
holeAnchors: mappedHoles,
|
||||
count: totalCount,
|
||||
itemBinding: self.itemGridBinding,
|
||||
headerText: headerText,
|
||||
snapTopInset: false
|
||||
)
|
||||
|
||||
self.itemCount = state.totalCount
|
||||
|
||||
let currentSynchronous = synchronous && firstTime
|
||||
let currentReloadAtTop = reloadAtTop && firstTime
|
||||
self.updateHistory(items: items, pinnedIds: state.pinnedIds, synchronous: currentSynchronous, reloadAtTop: currentReloadAtTop, animated: animated)
|
||||
}
|
||||
|
||||
private func updateHistory(items: SparseItemGrid.Items, pinnedIds: Set<Int32>, synchronous: Bool, reloadAtTop: Bool, animated: Bool) {
|
||||
var transition: ContainedViewLayoutTransition = .immediate
|
||||
if case .location = self.scope, let previousItems = self.items, previousItems.items.count == 0, previousItems.count != 0, items.items.count == 0, items.count == 0 {
|
||||
transition = .animated(duration: 0.3, curve: .spring)
|
||||
@ -2747,7 +2826,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
if reloadAtTop {
|
||||
gridSnapshot = self.itemGrid.view.snapshotView(afterScreenUpdates: false)
|
||||
}
|
||||
self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: false, transition: transition)
|
||||
self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: false, transition: transition, animateGridItems: animated)
|
||||
self.updateSelectedItems(animated: false)
|
||||
if let gridSnapshot = gridSnapshot {
|
||||
self.view.addSubview(gridSnapshot)
|
||||
@ -2765,6 +2844,40 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
}
|
||||
}
|
||||
|
||||
private func reorderIfPossible(item: SparseItemGrid.Item, toIndex: Int) {
|
||||
if case .botPreview = self.scope, let items = self.items, let item = item as? VisualMediaItem {
|
||||
guard let toItem = items.items.first(where: { $0.index == toIndex }) as? VisualMediaItem else {
|
||||
return
|
||||
}
|
||||
if item.story.isPending || toItem.story.isPending {
|
||||
return
|
||||
}
|
||||
|
||||
var ids = items.items.compactMap { item -> StoryId? in
|
||||
return (item as? VisualMediaItem)?.storyId
|
||||
}
|
||||
|
||||
if let fromIndex = ids.firstIndex(of: item.storyId) {
|
||||
if fromIndex < toIndex {
|
||||
ids.insert(item.storyId, at: toIndex + 1)
|
||||
ids.remove(at: fromIndex)
|
||||
} else if fromIndex > toIndex {
|
||||
ids.remove(at: fromIndex)
|
||||
ids.insert(item.storyId, at: toIndex)
|
||||
}
|
||||
}
|
||||
if self.reorderedIds != ids {
|
||||
self.reorderedIds = ids
|
||||
|
||||
HapticFeedback().tap()
|
||||
|
||||
if let currentListState = self.currentListState {
|
||||
self.updateItemsFromState(state: currentListState, firstTime: false, reloadAtTop: false, synchronous: false, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func scrollToTop() -> Bool {
|
||||
return self.itemGrid.scrollToTop()
|
||||
}
|
||||
@ -2849,6 +2962,46 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
return nil*/
|
||||
}
|
||||
|
||||
public func extractPendingStoryTransitionView() -> UIView? {
|
||||
guard let items = self.items else {
|
||||
return nil
|
||||
}
|
||||
guard let visualItem = items.items.last(where: { item in
|
||||
guard let item = item as? VisualMediaItem else {
|
||||
return false
|
||||
}
|
||||
if item.story.isPending {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}) else {
|
||||
return nil
|
||||
}
|
||||
guard let item = self.itemGrid.item(at: visualItem.index) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let itemLayer = item.layer as? ItemLayer else {
|
||||
return nil
|
||||
}
|
||||
guard let story = itemLayer.item?.story else {
|
||||
return nil
|
||||
}
|
||||
let rect = self.itemGrid.frameForItem(layer: itemLayer)
|
||||
|
||||
let tempSourceNode = TempExtractedItemNode(
|
||||
item: story,
|
||||
itemLayer: itemLayer
|
||||
)
|
||||
tempSourceNode.frame = rect
|
||||
tempSourceNode.update(size: rect.size)
|
||||
|
||||
self.tempContextContentItemNode = tempSourceNode
|
||||
self.view.addSubview(tempSourceNode.view)
|
||||
|
||||
return tempSourceNode.view
|
||||
}
|
||||
|
||||
public func addToTransitionSurface(view: UIView) {
|
||||
self.itemGrid.addToTransitionSurface(view: view)
|
||||
}
|
||||
@ -2982,7 +3135,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
}
|
||||
|
||||
private func updateSelectedItems(animated: Bool) {
|
||||
self.contextGestureContainerNode.isGestureEnabled = self.isProfileEmbedded && self.itemInteraction.selectedIds == nil
|
||||
self.contextGestureContainerNode.isGestureEnabled = self.isProfileEmbedded && self.itemInteraction.selectedIds == nil && !self.isReordering
|
||||
|
||||
self.itemGrid.forEachVisibleItem { item in
|
||||
guard let itemLayer = item.layer as? ItemLayer, let item = itemLayer.item else {
|
||||
@ -3304,6 +3457,10 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
}
|
||||
|
||||
public func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) {
|
||||
self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, deviceMetrics: deviceMetrics, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, navigationHeight: navigationHeight, presentationData: presentationData, synchronous: synchronous, transition: transition, animateGridItems: false)
|
||||
}
|
||||
|
||||
private func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, deviceMetrics: DeviceMetrics, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, navigationHeight: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition, animateGridItems: Bool) {
|
||||
self.currentParams = (size, topInset, sideInset, bottomInset, deviceMetrics, visibleHeight, isScrollingLockedAtTop, expandProgress, navigationHeight, presentationData)
|
||||
|
||||
var gridTopInset = topInset
|
||||
@ -3697,7 +3854,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
let fixedItemAspect: CGFloat? = 0.81
|
||||
|
||||
self.itemGrid.pinchEnabled = items.count > 2
|
||||
self.itemGrid.update(size: size, insets: UIEdgeInsets(top: gridTopInset, left: sideInset, bottom: bottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none)
|
||||
self.itemGrid.update(size: size, insets: UIEdgeInsets(top: gridTopInset, left: sideInset, bottom: bottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none, transition: animateGridItems ? .spring(duration: 0.35) : .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
@ -3788,12 +3945,45 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
|
||||
}
|
||||
|
||||
var maxCount = 10
|
||||
if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["appConfig.bot_preview_medias_max"] as? Double {
|
||||
if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["bot_preview_medias_max"] as? Double {
|
||||
maxCount = Int(value)
|
||||
}
|
||||
|
||||
return items.count < maxCount
|
||||
}
|
||||
|
||||
public func beginReordering() {
|
||||
if let parentController = self.parentController as? PeerInfoScreen {
|
||||
parentController.togglePaneIsReordering(isReordering: true)
|
||||
} else {
|
||||
self.updateIsReordering(isReordering: true, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
public func endReordering() {
|
||||
if let parentController = self.parentController as? PeerInfoScreen {
|
||||
parentController.togglePaneIsReordering(isReordering: false)
|
||||
} else {
|
||||
self.updateIsReordering(isReordering: false, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
public func updateIsReordering(isReordering: Bool, animated: Bool) {
|
||||
if self.isReordering != isReordering {
|
||||
self.isReordering = isReordering
|
||||
|
||||
self.contextGestureContainerNode.isGestureEnabled = self.isProfileEmbedded && self.itemInteraction.selectedIds == nil && !self.isReordering
|
||||
|
||||
self.itemGrid.setReordering(isReordering: isReordering)
|
||||
|
||||
if !isReordering, let reorderedIds = self.reorderedIds {
|
||||
self.reorderedIds = nil
|
||||
if case .botPreview = self.scope, let listSource = self.listSource as? BotPreviewStoryListContext {
|
||||
listSource.reorderItems(ids: reorderedIds)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class MediaListSelectionRecognizer: UIPanGestureRecognizer {
|
||||
@ -4033,7 +4223,6 @@ private final class BottomActionsPanelComponent: Component {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
|
||||
func update(component: BottomActionsPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
let themeUpdated = self.component?.theme !== component.theme
|
||||
|
||||
|
@ -331,6 +331,10 @@ private final class GenericItemLayer: CALayer, ItemLayer {
|
||||
|
||||
self.contentsGravity = .resize
|
||||
}
|
||||
|
||||
override init(layer: Any) {
|
||||
super.init(layer: layer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
@ -995,6 +999,9 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme
|
||||
return .never()
|
||||
}
|
||||
}
|
||||
|
||||
func reorderIfPossible(item: SparseItemGrid.Item, toIndex: Int) {
|
||||
}
|
||||
|
||||
func onTap(item: SparseItemGrid.Item, itemLayer: CALayer, point: CGPoint) {
|
||||
guard let item = item as? VisualMediaItem else {
|
||||
|
19
submodules/TelegramUI/Components/SpaceWarpView/BUILD
Normal file
19
submodules/TelegramUI/Components/SpaceWarpView/BUILD
Normal file
@ -0,0 +1,19 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "SpaceWarpView",
|
||||
module_name = "SpaceWarpView",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display",
|
||||
"//submodules/AsyncDisplayKit",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
|
||||
open class SpaceWarpView: UIView {
|
||||
|
||||
}
|
@ -1669,6 +1669,18 @@ private final class StoryContainerScreenComponent: Component {
|
||||
}
|
||||
component.content.markAsSeen(id: id)
|
||||
},
|
||||
reorder: { [weak self] in
|
||||
guard let self, let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
var performReorderAction: (() -> Void)?
|
||||
if let controller = environment.controller() as? StoryContainerScreen {
|
||||
performReorderAction = controller.performReorderAction
|
||||
}
|
||||
environment.controller()?.dismiss(completion: {
|
||||
performReorderAction?()
|
||||
})
|
||||
},
|
||||
controller: { [weak self] in
|
||||
return self?.environment?.controller()
|
||||
},
|
||||
@ -2027,6 +2039,7 @@ public class StoryContainerScreen: ViewControllerComponentContainer {
|
||||
}
|
||||
|
||||
public var customBackAction: (() -> Void)?
|
||||
public var performReorderAction: (() -> Void)?
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
|
@ -118,6 +118,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
public let navigate: (NavigationDirection) -> Void
|
||||
public let delete: () -> Void
|
||||
public let markAsSeen: (StoryId) -> Void
|
||||
public let reorder: () -> Void
|
||||
public let controller: () -> ViewController?
|
||||
public let toggleAmbientMode: () -> Void
|
||||
public let keyboardInputData: Signal<ChatEntityKeyboardInputNode.InputData, NoError>
|
||||
@ -154,6 +155,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
navigate: @escaping (NavigationDirection) -> Void,
|
||||
delete: @escaping () -> Void,
|
||||
markAsSeen: @escaping (StoryId) -> Void,
|
||||
reorder: @escaping () -> Void,
|
||||
controller: @escaping () -> ViewController?,
|
||||
toggleAmbientMode: @escaping () -> Void,
|
||||
keyboardInputData: Signal<ChatEntityKeyboardInputNode.InputData, NoError>,
|
||||
@ -189,6 +191,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
self.navigate = navigate
|
||||
self.delete = delete
|
||||
self.markAsSeen = markAsSeen
|
||||
self.reorder = reorder
|
||||
self.controller = controller
|
||||
self.toggleAmbientMode = toggleAmbientMode
|
||||
self.keyboardInputData = keyboardInputData
|
||||
@ -5678,6 +5681,14 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
component.presentController(actionSheet, nil)
|
||||
}
|
||||
|
||||
private func performReorderAction() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
component.reorder()
|
||||
}
|
||||
|
||||
private func performLikeAction() {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
@ -6566,7 +6577,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = component
|
||||
component.reorder()
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: "Edit Preview", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor)
|
||||
|
@ -12,38 +12,39 @@ import TelegramNotices
|
||||
import PresentationDataUtils
|
||||
import UndoUI
|
||||
import UrlHandling
|
||||
import TelegramPresentationData
|
||||
|
||||
public extension ChatControllerImpl {
|
||||
func openWebApp(buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource) {
|
||||
guard let peerId = self.chatLocation.peerId, let peer = self.presentationInterfaceState.renderedPeer?.peer else {
|
||||
return
|
||||
}
|
||||
let context = self.context
|
||||
|
||||
self.chatDisplayNode.dismissInput()
|
||||
|
||||
let botName: String
|
||||
let botAddress: String
|
||||
let botVerified: Bool
|
||||
if case let .inline(bot) = source {
|
||||
botName = bot.compactDisplayTitle
|
||||
botAddress = bot.addressName ?? ""
|
||||
botVerified = bot.isVerified
|
||||
} else {
|
||||
botName = EnginePeer(peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)
|
||||
botAddress = peer.addressName ?? ""
|
||||
botVerified = peer.isVerified
|
||||
}
|
||||
|
||||
if source == .generic {
|
||||
self.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||||
func openWebAppImpl(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool) {
|
||||
let presentationData: PresentationData
|
||||
if let parentController = parentController as? ChatControllerImpl {
|
||||
presentationData = parentController.presentationData
|
||||
} else {
|
||||
presentationData = context.sharedContext.currentPresentationData.with({ $0 })
|
||||
}
|
||||
|
||||
let botName: String
|
||||
let botAddress: String
|
||||
let botVerified: Bool
|
||||
if case let .inline(bot) = source {
|
||||
botName = bot.compactDisplayTitle
|
||||
botAddress = bot.addressName ?? ""
|
||||
botVerified = bot.isVerified
|
||||
} else {
|
||||
botName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||||
botAddress = peer.addressName ?? ""
|
||||
botVerified = peer.isVerified
|
||||
}
|
||||
|
||||
if source == .generic {
|
||||
if let parentController = parentController as? ChatControllerImpl {
|
||||
parentController.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||||
return $0.updatedTitlePanelContext {
|
||||
if !$0.contains(where: {
|
||||
switch $0 {
|
||||
case .requestInProgress:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
case .requestInProgress:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}) {
|
||||
var updatedContexts = $0
|
||||
@ -54,202 +55,263 @@ public extension ChatControllerImpl {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let updateProgress = { [weak self] in
|
||||
Queue.mainQueue().async {
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||||
return $0.updatedTitlePanelContext {
|
||||
if let index = $0.firstIndex(where: {
|
||||
switch $0 {
|
||||
case .requestInProgress:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}) {
|
||||
var updatedContexts = $0
|
||||
updatedContexts.remove(at: index)
|
||||
return updatedContexts
|
||||
}
|
||||
|
||||
let updateProgress = { [weak parentController] in
|
||||
Queue.mainQueue().async {
|
||||
if let parentController = parentController as? ChatControllerImpl {
|
||||
parentController.updateChatPresentationInterfaceState(animated: true, interactive: true, {
|
||||
return $0.updatedTitlePanelContext {
|
||||
if let index = $0.firstIndex(where: {
|
||||
switch $0 {
|
||||
case .requestInProgress:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return $0
|
||||
}) {
|
||||
var updatedContexts = $0
|
||||
updatedContexts.remove(at: index)
|
||||
return updatedContexts
|
||||
}
|
||||
})
|
||||
}
|
||||
return $0
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let openWebView = {
|
||||
if source == .menu {
|
||||
self.updateChatPresentationInterfaceState(interactive: false) { state in
|
||||
}
|
||||
|
||||
|
||||
let openWebView = { [weak parentController] in
|
||||
guard let parentController else {
|
||||
return
|
||||
}
|
||||
if source == .menu {
|
||||
if let parentController = parentController as? ChatControllerImpl {
|
||||
parentController.updateChatPresentationInterfaceState(interactive: false) { state in
|
||||
return state.updatedForceInputCommandsHidden(true)
|
||||
}
|
||||
|
||||
if let navigationController = self.navigationController as? NavigationController, let minimizedContainer = navigationController.minimizedContainer {
|
||||
for controller in minimizedContainer.controllers {
|
||||
if let controller = controller as? AttachmentController, let mainController = controller.mainController as? WebAppController, mainController.botId == peerId && mainController.source == .menu {
|
||||
navigationController.maximizeViewController(controller, animated: true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if let navigationController = parentController.navigationController as? NavigationController, let minimizedContainer = navigationController.minimizedContainer {
|
||||
for controller in minimizedContainer.controllers {
|
||||
if let controller = controller as? AttachmentController, let mainController = controller.mainController as? WebAppController, mainController.botId == peer.id && mainController.source == .menu {
|
||||
navigationController.maximizeViewController(controller, animated: true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var fullSize = false
|
||||
if isTelegramMeLink(url), let internalUrl = parseFullInternalUrl(sharedContext: self.context.sharedContext, url: url), case .peer(_, .appStart) = internalUrl {
|
||||
fullSize = !url.contains("?mode=compact")
|
||||
}
|
||||
}
|
||||
|
||||
var fullSize = false
|
||||
if isTelegramMeLink(url), let internalUrl = parseFullInternalUrl(sharedContext: context.sharedContext, url: url), case .peer(_, .appStart) = internalUrl {
|
||||
fullSize = !url.contains("?mode=compact")
|
||||
}
|
||||
|
||||
var presentImpl: ((ViewController, Any?) -> Void)?
|
||||
let params = WebAppParameters(source: .menu, peerId: peerId, botId: peerId, botName: botName, botVerified: botVerified, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: fullSize)
|
||||
let controller = standaloneWebAppController(context: self.context, updatedPresentationData: self.updatedPresentationData, params: params, threadId: self.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in
|
||||
ChatControllerImpl.botOpenUrl(context: context, peerId: peerId, controller: self, url: url, concealed: concealed, present: { c, a in
|
||||
presentImpl?(c, a)
|
||||
}, commit: commit)
|
||||
}, requestSwitchInline: { [weak self] query, chatTypes, completion in
|
||||
ChatControllerImpl.botRequestSwitchInline(context: context, controller: self, peerId: peerId, botAddress: botAddress, query: query, chatTypes: chatTypes, completion: completion)
|
||||
}, getInputContainerNode: { [weak self] in
|
||||
if let strongSelf = self, let layout = strongSelf.validLayout, case .compact = layout.metrics.widthClass {
|
||||
return (strongSelf.chatDisplayNode.getWindowInputAccessoryHeight(), strongSelf.chatDisplayNode.inputPanelContainerNode, {
|
||||
return strongSelf.chatDisplayNode.textInputPanelNode?.makeAttachmentMenuTransition(accessoryPanelNode: nil)
|
||||
})
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}, completion: { [weak self] in
|
||||
self?.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
||||
}, willDismiss: { [weak self] in
|
||||
self?.interfaceInteraction?.updateShowWebView { _ in
|
||||
var presentImpl: ((ViewController, Any?) -> Void)?
|
||||
let params = WebAppParameters(source: .menu, peerId: peer.id, botId: peer.id, botName: botName, botVerified: botVerified, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: fullSize)
|
||||
let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, commit in
|
||||
ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, present: { c, a in
|
||||
presentImpl?(c, a)
|
||||
}, commit: commit)
|
||||
}, requestSwitchInline: { [weak parentController] query, chatTypes, completion in
|
||||
ChatControllerImpl.botRequestSwitchInline(context: context, controller: parentController as? ChatControllerImpl, peerId: peer.id, botAddress: botAddress, query: query, chatTypes: chatTypes, completion: completion)
|
||||
}, getInputContainerNode: { [weak parentController] in
|
||||
if let parentController = parentController as? ChatControllerImpl, let layout = parentController.validLayout, case .compact = layout.metrics.widthClass {
|
||||
return (parentController.chatDisplayNode.getWindowInputAccessoryHeight(), parentController.chatDisplayNode.inputPanelContainerNode, {
|
||||
return parentController.chatDisplayNode.textInputPanelNode?.makeAttachmentMenuTransition(accessoryPanelNode: nil)
|
||||
})
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}, completion: { [weak parentController] in
|
||||
if let parentController = parentController as? ChatControllerImpl {
|
||||
parentController.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
||||
}
|
||||
}, willDismiss: { [weak parentController] in
|
||||
if let parentController = parentController as? ChatControllerImpl {
|
||||
parentController.interfaceInteraction?.updateShowWebView { _ in
|
||||
return false
|
||||
}
|
||||
}, didDismiss: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
let isFocused = strongSelf.chatDisplayNode.textInputPanelNode?.isFocused ?? false
|
||||
strongSelf.chatDisplayNode.insertSubnode(strongSelf.chatDisplayNode.inputPanelContainerNode, aboveSubnode: strongSelf.chatDisplayNode.inputContextPanelContainer)
|
||||
if isFocused {
|
||||
strongSelf.chatDisplayNode.textInputPanelNode?.ensureFocused()
|
||||
}
|
||||
|
||||
strongSelf.updateChatPresentationInterfaceState(interactive: false) { state in
|
||||
return state.updatedForceInputCommandsHidden(false)
|
||||
}
|
||||
}
|
||||
}, didDismiss: { [weak parentController] in
|
||||
if let parentController = parentController as? ChatControllerImpl {
|
||||
let isFocused = parentController.chatDisplayNode.textInputPanelNode?.isFocused ?? false
|
||||
parentController.chatDisplayNode.insertSubnode(parentController.chatDisplayNode.inputPanelContainerNode, aboveSubnode: parentController.chatDisplayNode.inputContextPanelContainer)
|
||||
if isFocused {
|
||||
parentController.chatDisplayNode.textInputPanelNode?.ensureFocused()
|
||||
}
|
||||
|
||||
parentController.updateChatPresentationInterfaceState(interactive: false) { state in
|
||||
return state.updatedForceInputCommandsHidden(false)
|
||||
}
|
||||
}
|
||||
}, getNavigationController: { [weak parentController] in
|
||||
if let parentController = parentController as? ChatControllerImpl {
|
||||
return parentController.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController
|
||||
} else {
|
||||
return parentController?.navigationController as? NavigationController
|
||||
}
|
||||
})
|
||||
controller.navigationPresentation = .flatModal
|
||||
parentController.push(controller)
|
||||
|
||||
presentImpl = { [weak controller] c, a in
|
||||
controller?.present(c, in: .window(.root), with: a)
|
||||
}
|
||||
} else if simple {
|
||||
var isInline = false
|
||||
var botId = peer.id
|
||||
var botName = botName
|
||||
var botAddress = ""
|
||||
var botVerified = false
|
||||
if case let .inline(bot) = source {
|
||||
isInline = true
|
||||
botId = bot.id
|
||||
botName = bot.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
|
||||
botAddress = bot.addressName ?? ""
|
||||
botVerified = bot.isVerified
|
||||
}
|
||||
|
||||
let messageActionCallbackDisposable: MetaDisposable
|
||||
if let parentController = parentController as? ChatControllerImpl {
|
||||
messageActionCallbackDisposable = parentController.messageActionCallbackDisposable
|
||||
} else {
|
||||
messageActionCallbackDisposable = MetaDisposable()
|
||||
}
|
||||
|
||||
let webViewSignal: Signal<RequestWebViewResult, RequestWebViewError>
|
||||
if url.isEmpty {
|
||||
webViewSignal = context.engine.messages.requestMainWebView(botId: botId, source: isInline ? .inline : .generic, themeParams: generateWebAppThemeParams(presentationData.theme))
|
||||
} else {
|
||||
webViewSignal = context.engine.messages.requestSimpleWebView(botId: botId, url: url, source: isInline ? .inline : .generic, themeParams: generateWebAppThemeParams(presentationData.theme))
|
||||
}
|
||||
|
||||
messageActionCallbackDisposable.set(((webViewSignal
|
||||
|> afterDisposed {
|
||||
updateProgress()
|
||||
})
|
||||
|> deliverOnMainQueue).start(next: { [weak parentController] result in
|
||||
guard let parentController else {
|
||||
return
|
||||
}
|
||||
var presentImpl: ((ViewController, Any?) -> Void)?
|
||||
let params = WebAppParameters(source: isInline ? .inline : .simple, peerId: peer.id, botId: botId, botName: botName, botVerified: botVerified, url: result.url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize))
|
||||
let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, commit in
|
||||
ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, present: { c, a in
|
||||
presentImpl?(c, a)
|
||||
}, commit: commit)
|
||||
}, requestSwitchInline: { [weak parentController] query, chatTypes, completion in
|
||||
ChatControllerImpl.botRequestSwitchInline(context: context, controller: parentController as? ChatControllerImpl, peerId: peer.id, botAddress: botAddress, query: query, chatTypes: chatTypes, completion: completion)
|
||||
}, getNavigationController: { [weak parentController] in
|
||||
if let parentController = parentController as? ChatControllerImpl {
|
||||
return parentController.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController
|
||||
} else {
|
||||
return parentController?.navigationController as? NavigationController
|
||||
}
|
||||
}, getNavigationController: { [weak self] in
|
||||
return self?.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController
|
||||
})
|
||||
controller.navigationPresentation = .flatModal
|
||||
self.push(controller)
|
||||
if let parentController = parentController as? ChatControllerImpl {
|
||||
parentController.currentWebAppController = controller
|
||||
}
|
||||
parentController.push(controller)
|
||||
|
||||
presentImpl = { [weak controller] c, a in
|
||||
controller?.present(c, in: .window(.root), with: a)
|
||||
}
|
||||
} else if simple {
|
||||
var isInline = false
|
||||
var botId = peerId
|
||||
var botName = botName
|
||||
var botAddress = ""
|
||||
var botVerified = false
|
||||
if case let .inline(bot) = source {
|
||||
isInline = true
|
||||
botId = bot.id
|
||||
botName = bot.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)
|
||||
botAddress = bot.addressName ?? ""
|
||||
botVerified = bot.isVerified
|
||||
}, error: { [weak parentController] error in
|
||||
if let parentController {
|
||||
parentController.present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
|
||||
})]), in: .window(.root))
|
||||
}
|
||||
|
||||
self.messageActionCallbackDisposable.set(((self.context.engine.messages.requestSimpleWebView(botId: botId, url: url, source: isInline ? .inline : .generic, themeParams: generateWebAppThemeParams(self.presentationData.theme))
|
||||
|> afterDisposed {
|
||||
updateProgress()
|
||||
})
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] result in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
var presentImpl: ((ViewController, Any?) -> Void)?
|
||||
let context = strongSelf.context
|
||||
let params = WebAppParameters(source: isInline ? .inline : .simple, peerId: peerId, botId: botId, botName: botName, botVerified: botVerified, url: result.url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, forceHasSettings: false, fullSize: result.flags.contains(.fullSize))
|
||||
let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in
|
||||
ChatControllerImpl.botOpenUrl(context: context, peerId: peerId, controller: self, url: url, concealed: concealed, present: { c, a in
|
||||
presentImpl?(c, a)
|
||||
}, commit: commit)
|
||||
}, requestSwitchInline: { [weak self] query, chatTypes, completion in
|
||||
ChatControllerImpl.botRequestSwitchInline(context: context, controller: self, peerId: peerId, botAddress: botAddress, query: query, chatTypes: chatTypes, completion: completion)
|
||||
}, getNavigationController: { [weak self] in
|
||||
return self?.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController
|
||||
})
|
||||
controller.navigationPresentation = .flatModal
|
||||
strongSelf.currentWebAppController = controller
|
||||
strongSelf.push(controller)
|
||||
|
||||
presentImpl = { [weak controller] c, a in
|
||||
controller?.present(c, in: .window(.root), with: a)
|
||||
}
|
||||
}, error: { [weak self] error in
|
||||
if let strongSelf = self {
|
||||
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {
|
||||
})]), in: .window(.root))
|
||||
}
|
||||
}))
|
||||
}))
|
||||
} else {
|
||||
let messageActionCallbackDisposable: MetaDisposable
|
||||
if let parentController = parentController as? ChatControllerImpl {
|
||||
messageActionCallbackDisposable = parentController.messageActionCallbackDisposable
|
||||
} else {
|
||||
self.messageActionCallbackDisposable.set(((self.context.engine.messages.requestWebView(peerId: peerId, botId: peerId, url: !url.isEmpty ? url : nil, payload: nil, themeParams: generateWebAppThemeParams(self.presentationData.theme), fromMenu: false, replyToMessageId: nil, threadId: self.chatLocation.threadId)
|
||||
|> afterDisposed {
|
||||
updateProgress()
|
||||
})
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] result in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
var presentImpl: ((ViewController, Any?) -> Void)?
|
||||
let context = strongSelf.context
|
||||
let params = WebAppParameters(source: .button, peerId: peerId, botId: peerId, botName: botName, botVerified: botVerified, url: result.url, queryId: result.queryId, payload: nil, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, forceHasSettings: false, fullSize: result.flags.contains(.fullSize))
|
||||
let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in
|
||||
ChatControllerImpl.botOpenUrl(context: context, peerId: peerId, controller: self, url: url, concealed: concealed, present: { c, a in
|
||||
presentImpl?(c, a)
|
||||
}, commit: commit)
|
||||
}, completion: { [weak self] in
|
||||
self?.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
||||
}, getNavigationController: { [weak self] in
|
||||
return self?.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController
|
||||
})
|
||||
controller.navigationPresentation = .flatModal
|
||||
strongSelf.currentWebAppController = controller
|
||||
strongSelf.push(controller)
|
||||
|
||||
presentImpl = { [weak controller] c, a in
|
||||
controller?.present(c, in: .window(.root), with: a)
|
||||
}
|
||||
}, error: { [weak self] error in
|
||||
if let strongSelf = self {
|
||||
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: nil, text: strongSelf.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {
|
||||
})]), in: .window(.root))
|
||||
}
|
||||
}))
|
||||
messageActionCallbackDisposable = MetaDisposable()
|
||||
}
|
||||
|
||||
messageActionCallbackDisposable.set(((context.engine.messages.requestWebView(peerId: peer.id, botId: peer.id, url: !url.isEmpty ? url : nil, payload: nil, themeParams: generateWebAppThemeParams(presentationData.theme), fromMenu: false, replyToMessageId: nil, threadId: threadId)
|
||||
|> afterDisposed {
|
||||
updateProgress()
|
||||
})
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak parentController] result in
|
||||
guard let parentController else {
|
||||
return
|
||||
}
|
||||
var presentImpl: ((ViewController, Any?) -> Void)?
|
||||
let params = WebAppParameters(source: .button, peerId: peer.id, botId: peer.id, botName: botName, botVerified: botVerified, url: result.url, queryId: result.queryId, payload: nil, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, forceHasSettings: false, fullSize: result.flags.contains(.fullSize))
|
||||
let controller = standaloneWebAppController(context: context, updatedPresentationData: updatedPresentationData, params: params, threadId: threadId, openUrl: { [weak parentController] url, concealed, commit in
|
||||
ChatControllerImpl.botOpenUrl(context: context, peerId: peer.id, controller: parentController as? ChatControllerImpl, url: url, concealed: concealed, present: { c, a in
|
||||
presentImpl?(c, a)
|
||||
}, commit: commit)
|
||||
}, completion: { [weak parentController] in
|
||||
if let parentController = parentController as? ChatControllerImpl {
|
||||
parentController.chatDisplayNode.historyNode.scrollToEndOfHistory()
|
||||
}
|
||||
}, getNavigationController: { [weak parentController] in
|
||||
if let parentController = parentController as? ChatControllerImpl {
|
||||
return parentController.effectiveNavigationController ?? context.sharedContext.mainWindow?.viewController as? NavigationController
|
||||
} else {
|
||||
return parentController?.navigationController as? NavigationController
|
||||
}
|
||||
})
|
||||
controller.navigationPresentation = .flatModal
|
||||
if let parentController = parentController as? ChatControllerImpl {
|
||||
parentController.currentWebAppController = controller
|
||||
}
|
||||
parentController.push(controller)
|
||||
|
||||
presentImpl = { [weak controller] c, a in
|
||||
controller?.present(c, in: .window(.root), with: a)
|
||||
}
|
||||
}, error: { [weak parentController] error in
|
||||
if let parentController {
|
||||
parentController.present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
|
||||
})]), in: .window(.root))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
var botPeer = EnginePeer(peer)
|
||||
}
|
||||
|
||||
if skipTermsOfService {
|
||||
openWebView()
|
||||
} else {
|
||||
var botPeer = peer
|
||||
if case let .inline(bot) = source {
|
||||
botPeer = bot
|
||||
}
|
||||
let _ = (ApplicationSpecificNotice.getBotGameNotice(accountManager: self.context.sharedContext.accountManager, peerId: botPeer.id)
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] value in
|
||||
guard let strongSelf = self else {
|
||||
let _ = (ApplicationSpecificNotice.getBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id)
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak parentController] value in
|
||||
guard let parentController else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if value {
|
||||
openWebView()
|
||||
} else {
|
||||
let controller = webAppLaunchConfirmationController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: botPeer, completion: { _ in
|
||||
let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: strongSelf.context.sharedContext.accountManager, peerId: botPeer.id).startStandalone()
|
||||
let controller = webAppLaunchConfirmationController(context: context, updatedPresentationData: updatedPresentationData, peer: botPeer, completion: { _ in
|
||||
let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).startStandalone()
|
||||
openWebView()
|
||||
}, showMore: nil)
|
||||
strongSelf.present(controller, in: .window(.root))
|
||||
parentController.present(controller, in: .window(.root))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public extension ChatControllerImpl {
|
||||
func openWebApp(buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource) {
|
||||
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
|
||||
return
|
||||
}
|
||||
self.chatDisplayNode.dismissInput()
|
||||
|
||||
self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, peer: EnginePeer(peer), threadId: self.chatLocation.threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: false)
|
||||
}
|
||||
|
||||
private static func botRequestSwitchInline(context: AccountContext, controller: ChatControllerImpl?, peerId: EnginePeer.Id, botAddress: String, query: String, chatTypes: [ReplyMarkupButtonRequestPeerType]?, completion: @escaping () -> Void) -> Void {
|
||||
static func botRequestSwitchInline(context: AccountContext, controller: ChatControllerImpl?, peerId: EnginePeer.Id, botAddress: String, query: String, chatTypes: [ReplyMarkupButtonRequestPeerType]?, completion: @escaping () -> Void) -> Void {
|
||||
let activateSwitchInline = {
|
||||
var chatController: ChatControllerImpl?
|
||||
if let current = controller {
|
||||
@ -311,7 +373,7 @@ public extension ChatControllerImpl {
|
||||
})
|
||||
}
|
||||
|
||||
private static func botOpenUrl(context: AccountContext, peerId: EnginePeer.Id, controller: ChatControllerImpl?, url: String, concealed: Bool, present: @escaping (ViewController, Any?) -> Void, commit: @escaping () -> Void = {}) {
|
||||
static func botOpenUrl(context: AccountContext, peerId: EnginePeer.Id, controller: ChatControllerImpl?, url: String, concealed: Bool, present: @escaping (ViewController, Any?) -> Void, commit: @escaping () -> Void = {}) {
|
||||
if let controller {
|
||||
controller.openUrl(url, concealed: concealed, forceExternal: true, commit: commit)
|
||||
} else {
|
||||
|
@ -2719,6 +2719,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
public func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController {
|
||||
return StarsTransactionScreen(context: context, subject: .gift(message), action: {})
|
||||
}
|
||||
|
||||
public func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peer: EnginePeer, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool) {
|
||||
openWebAppImpl(context: context, parentController: parentController, updatedPresentationData: updatedPresentationData, peer: peer, threadId: threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: skipTermsOfService)
|
||||
}
|
||||
}
|
||||
|
||||
private func peerInfoControllerImpl(context: AccountContext, updatedPresentationData: (PresentationData, Signal<PresentationData, NoError>)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, requestsContext: PeerInvitationImportersContext? = nil) -> ViewController? {
|
||||
|
@ -338,7 +338,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
return CameraScreen.TransitionOut(
|
||||
destinationView: destinationView,
|
||||
destinationRect: transitionOut.destinationRect,
|
||||
destinationCornerRadius: transitionOut.destinationCornerRadius
|
||||
destinationCornerRadius: transitionOut.destinationCornerRadius,
|
||||
completion: transitionOut.completion
|
||||
)
|
||||
} else {
|
||||
return nil
|
||||
@ -408,13 +409,15 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
return MediaEditorScreen.TransitionOut(
|
||||
destinationView: destinationView,
|
||||
destinationRect: transitionOut.destinationRect,
|
||||
destinationCornerRadius: transitionOut.destinationCornerRadius
|
||||
destinationCornerRadius: transitionOut.destinationCornerRadius,
|
||||
completion: transitionOut.completion
|
||||
)
|
||||
} else if !finished, let resultTransition, let (destinationView, destinationRect) = resultTransition.transitionOut(isNew) {
|
||||
return MediaEditorScreen.TransitionOut(
|
||||
destinationView: destinationView,
|
||||
destinationRect: destinationRect,
|
||||
destinationCornerRadius: 0.0
|
||||
destinationCornerRadius: 0.0,
|
||||
completion: nil
|
||||
)
|
||||
} else {
|
||||
return nil
|
||||
|
Loading…
x
Reference in New Issue
Block a user