Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2024-07-19 17:43:27 +04:00
commit 63714bec47
36 changed files with 1903 additions and 721 deletions

View File

@ -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?

View File

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

View File

@ -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 {

View File

@ -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()

View File

@ -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)

View File

@ -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,

View File

@ -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

View File

@ -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 {

View File

@ -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
}
}
}
}

View File

@ -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
}
}

View File

@ -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

View File

@ -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()

View File

@ -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)
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)

View File

@ -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": [],

View File

@ -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

View File

@ -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)

View File

@ -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 {

View File

@ -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,

View File

@ -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)
}
}
}

View File

@ -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))
}
})
}
}*/

View File

@ -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

View File

@ -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 {

View 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",
],
)

View File

@ -0,0 +1,8 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
open class SpaceWarpView: UIView {
}

View File

@ -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,

View File

@ -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)

View File

@ -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 {

View File

@ -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? {

View File

@ -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