mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Updates too many to describe
This commit is contained in:
parent
ba6cc80b60
commit
04efb74bfa
@ -5342,6 +5342,7 @@ Any member of this group will be able to see messages in the channel.";
|
||||
"PeerInfo.PaneAudio" = "Audio";
|
||||
"PeerInfo.PaneGroups" = "Groups";
|
||||
"PeerInfo.PaneMembers" = "Members";
|
||||
"PeerInfo.PaneGifs" = "GIFs";
|
||||
|
||||
"PeerInfo.AddToContacts" = "Add to Contacts";
|
||||
|
||||
@ -5499,3 +5500,7 @@ Any member of this group will be able to see messages in the channel.";
|
||||
"PeerInfo.GroupAboutItem" = "about";
|
||||
|
||||
"Widget.ApplicationStartRequired" = "Open the app to use the widget";
|
||||
|
||||
"ChatList.Context.AddToFolder" = "Add to Folder";
|
||||
"ChatList.Context.Back" = "Back";
|
||||
"ChatList.AddedToFolderTooltip" = "%1$@ has been added to folder %2$@";
|
||||
|
@ -3,6 +3,7 @@ import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
|
||||
public struct ChatListNodeAdditionalCategory {
|
||||
public var id: Int
|
||||
@ -30,7 +31,7 @@ public enum ContactMultiselectionControllerMode {
|
||||
case groupCreation
|
||||
case peerSelection(searchChatList: Bool, searchGroups: Bool, searchChannels: Bool)
|
||||
case channelCreation
|
||||
case chatSelection(title: String, selectedChats: Set<PeerId>, additionalCategories: ContactMultiselectionControllerAdditionalCategories?)
|
||||
case chatSelection(title: String, selectedChats: Set<PeerId>, additionalCategories: ContactMultiselectionControllerAdditionalCategories?, chatListFilters: [ChatListFilter]?)
|
||||
}
|
||||
|
||||
public enum ContactListFilter {
|
||||
|
@ -11,9 +11,11 @@ import TelegramUIPreferences
|
||||
import OverlayStatusController
|
||||
import AlertUI
|
||||
import PresentationDataUtils
|
||||
import UndoUI
|
||||
|
||||
func archiveContextMenuItems(context: AccountContext, groupId: PeerGroupId, chatListController: ChatListControllerImpl?) -> Signal<[ContextMenuItem], NoError> {
|
||||
let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings
|
||||
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
|
||||
let strings = presentationData.strings
|
||||
return context.account.postbox.transaction { [weak chatListController] transaction -> [ContextMenuItem] in
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
@ -45,7 +47,8 @@ enum ChatContextMenuSource {
|
||||
}
|
||||
|
||||
func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: ChatListNodeEntryPromoInfo?, source: ChatContextMenuSource, chatListController: ChatListControllerImpl?) -> Signal<[ContextMenuItem], NoError> {
|
||||
let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings
|
||||
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
|
||||
let strings = presentationData.strings
|
||||
return context.account.postbox.transaction { [weak chatListController] transaction -> [ContextMenuItem] in
|
||||
if promoInfo != nil {
|
||||
return []
|
||||
@ -107,6 +110,91 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
|
||||
}
|
||||
|
||||
if case .chatList = source {
|
||||
var hasFolders = false
|
||||
updateChatListFiltersInteractively(transaction: transaction, { filters in
|
||||
for filter in filters {
|
||||
var data = filter.data
|
||||
if data.addIncludePeer(peerId: peerId) {
|
||||
hasFolders = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return filters
|
||||
})
|
||||
|
||||
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
|
||||
let _ = (context.account.postbox.transaction { transaction -> [ContextMenuItem] in
|
||||
var updatedItems: [ContextMenuItem] = []
|
||||
updateChatListFiltersInteractively(transaction: transaction, { filters in
|
||||
for filter in filters {
|
||||
var data = filter.data
|
||||
if !data.addIncludePeer(peerId: peerId) {
|
||||
continue
|
||||
}
|
||||
|
||||
let filterType = chatListFilterType(filter)
|
||||
updatedItems.append(.action(ContextMenuActionItem(text: filter.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 _ = (updateChatListFiltersInteractively(postbox: context.account.postbox, { filters in
|
||||
var filters = filters
|
||||
for i in 0 ..< filters.count {
|
||||
if filters[i].id == filter.id {
|
||||
let _ = filters[i].data.addIncludePeer(peerId: peerId)
|
||||
break
|
||||
}
|
||||
}
|
||||
return filters
|
||||
})).start()
|
||||
|
||||
if let peer = peer {
|
||||
chatListController?.present(UndoOverlayController(presentationData: presentationData, content: .chatAddedToFolder(chatTitle: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), folderTitle: filter.title), elevatedLayout: false, animateInAsReplacement: true, action: { _ in
|
||||
return false
|
||||
}), in: .current)
|
||||
}
|
||||
})
|
||||
})))
|
||||
}
|
||||
|
||||
return filters
|
||||
})
|
||||
|
||||
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)
|
||||
}, action: { c, _ in
|
||||
c.setItems(chatContextMenuItems(context: context, peerId: peerId, promoInfo: promoInfo, source: source, chatListController: chatListController))
|
||||
})))
|
||||
|
||||
return updatedItems
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { updatedItems in
|
||||
c.setItems(.single(updatedItems))
|
||||
})
|
||||
})))
|
||||
}
|
||||
|
||||
if let readState = transaction.getCombinedPeerReadState(peerId), readState.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 _ = togglePeerUnreadMarkInteractively(postbox: context.account.postbox, viewTracker: context.account.viewTracker, peerId: peerId).start()
|
||||
|
@ -974,8 +974,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
|
||||
var found = false
|
||||
for filter in presetList {
|
||||
if filter.id == id {
|
||||
strongSelf.push(chatListFilterAddChatsController(context: strongSelf.context, filter: filter))
|
||||
f(.dismissWithoutContent)
|
||||
let _ = (currentChatListFilters(postbox: strongSelf.context.account.postbox)
|
||||
|> deliverOnMainQueue).start(next: { filters in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.push(chatListFilterAddChatsController(context: strongSelf.context, filter: filter, allFilters: filters))
|
||||
f(.dismissWithoutContent)
|
||||
})
|
||||
found = true
|
||||
break
|
||||
}
|
||||
|
@ -545,11 +545,11 @@ private enum AdditionalExcludeCategoryId: Int {
|
||||
case archived
|
||||
}
|
||||
|
||||
func chatListFilterAddChatsController(context: AccountContext, filter: ChatListFilter) -> ViewController {
|
||||
return internalChatListFilterAddChatsController(context: context, filter: filter, applyAutomatically: true, updated: { _ in })
|
||||
func chatListFilterAddChatsController(context: AccountContext, filter: ChatListFilter, allFilters: [ChatListFilter]) -> ViewController {
|
||||
return internalChatListFilterAddChatsController(context: context, filter: filter, allFilters: allFilters, applyAutomatically: true, updated: { _ in })
|
||||
}
|
||||
|
||||
private func internalChatListFilterAddChatsController(context: AccountContext, filter: ChatListFilter, applyAutomatically: Bool, updated: @escaping (ChatListFilter) -> Void) -> ViewController {
|
||||
private func internalChatListFilterAddChatsController(context: AccountContext, filter: ChatListFilter, allFilters: [ChatListFilter], applyAutomatically: Bool, updated: @escaping (ChatListFilter) -> Void) -> ViewController {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let additionalCategories: [ChatListNodeAdditionalCategory] = [
|
||||
ChatListNodeAdditionalCategory(
|
||||
@ -592,7 +592,7 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
|
||||
}
|
||||
}
|
||||
|
||||
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .chatSelection(title: presentationData.strings.ChatListFolder_IncludeChatsTitle, selectedChats: Set(filter.data.includePeers.peers), additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories)), options: [], filters: [], alwaysEnabled: true))
|
||||
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .chatSelection(title: presentationData.strings.ChatListFolder_IncludeChatsTitle, selectedChats: Set(filter.data.includePeers.peers), additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories), chatListFilters: allFilters), options: [], filters: [], alwaysEnabled: true))
|
||||
controller.navigationPresentation = .modal
|
||||
let _ = (controller.result
|
||||
|> take(1)
|
||||
@ -649,7 +649,7 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
|
||||
return controller
|
||||
}
|
||||
|
||||
private func internalChatListFilterExcludeChatsController(context: AccountContext, filter: ChatListFilter, applyAutomatically: Bool, updated: @escaping (ChatListFilter) -> Void) -> ViewController {
|
||||
private func internalChatListFilterExcludeChatsController(context: AccountContext, filter: ChatListFilter, allFilters: [ChatListFilter], applyAutomatically: Bool, updated: @escaping (ChatListFilter) -> Void) -> ViewController {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let additionalCategories: [ChatListNodeAdditionalCategory] = [
|
||||
ChatListNodeAdditionalCategory(
|
||||
@ -679,7 +679,7 @@ private func internalChatListFilterExcludeChatsController(context: AccountContex
|
||||
selectedCategories.insert(AdditionalExcludeCategoryId.archived.rawValue)
|
||||
}
|
||||
|
||||
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .chatSelection(title: presentationData.strings.ChatListFolder_ExcludeChatsTitle, selectedChats: Set(filter.data.excludePeers), additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories)), options: [], filters: [], alwaysEnabled: true))
|
||||
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .chatSelection(title: presentationData.strings.ChatListFolder_ExcludeChatsTitle, selectedChats: Set(filter.data.excludePeers), additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories), chatListFilters: allFilters), options: [], filters: [], alwaysEnabled: true))
|
||||
controller.navigationPresentation = .modal
|
||||
let _ = (controller.result
|
||||
|> take(1)
|
||||
@ -834,17 +834,20 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
|
||||
includePeers.setPeers(state.additionallyIncludePeers)
|
||||
let filter = ChatListFilter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers))
|
||||
|
||||
let controller = internalChatListFilterAddChatsController(context: context, filter: filter, applyAutomatically: false, updated: { filter in
|
||||
skipStateAnimation = true
|
||||
updateState { state in
|
||||
var state = state
|
||||
state.additionallyIncludePeers = filter.data.includePeers.peers
|
||||
state.additionallyExcludePeers = filter.data.excludePeers
|
||||
state.includeCategories = filter.data.categories
|
||||
return state
|
||||
}
|
||||
let _ = (currentChatListFilters(postbox: context.account.postbox)
|
||||
|> deliverOnMainQueue).start(next: { filters in
|
||||
let controller = internalChatListFilterAddChatsController(context: context, filter: filter, allFilters: filters, applyAutomatically: false, updated: { filter in
|
||||
skipStateAnimation = true
|
||||
updateState { state in
|
||||
var state = state
|
||||
state.additionallyIncludePeers = filter.data.includePeers.peers
|
||||
state.additionallyExcludePeers = filter.data.excludePeers
|
||||
state.includeCategories = filter.data.categories
|
||||
return state
|
||||
}
|
||||
})
|
||||
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
})
|
||||
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
},
|
||||
openAddExcludePeer: {
|
||||
let state = stateValue.with { $0 }
|
||||
@ -852,20 +855,23 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
|
||||
includePeers.setPeers(state.additionallyIncludePeers)
|
||||
let filter = ChatListFilter(id: currentPreset?.id ?? -1, title: state.name, emoticon: currentPreset?.emoticon, data: ChatListFilterData(categories: state.includeCategories, excludeMuted: state.excludeMuted, excludeRead: state.excludeRead, excludeArchived: state.excludeArchived, includePeers: includePeers, excludePeers: state.additionallyExcludePeers))
|
||||
|
||||
let controller = internalChatListFilterExcludeChatsController(context: context, filter: filter, applyAutomatically: false, updated: { filter in
|
||||
skipStateAnimation = true
|
||||
updateState { state in
|
||||
var state = state
|
||||
state.additionallyIncludePeers = filter.data.includePeers.peers
|
||||
state.additionallyExcludePeers = filter.data.excludePeers
|
||||
state.includeCategories = filter.data.categories
|
||||
state.excludeRead = filter.data.excludeRead
|
||||
state.excludeMuted = filter.data.excludeMuted
|
||||
state.excludeArchived = filter.data.excludeArchived
|
||||
return state
|
||||
}
|
||||
let _ = (currentChatListFilters(postbox: context.account.postbox)
|
||||
|> deliverOnMainQueue).start(next: { filters in
|
||||
let controller = internalChatListFilterExcludeChatsController(context: context, filter: filter, allFilters: filters, applyAutomatically: false, updated: { filter in
|
||||
skipStateAnimation = true
|
||||
updateState { state in
|
||||
var state = state
|
||||
state.additionallyIncludePeers = filter.data.includePeers.peers
|
||||
state.additionallyExcludePeers = filter.data.excludePeers
|
||||
state.includeCategories = filter.data.categories
|
||||
state.excludeRead = filter.data.excludeRead
|
||||
state.excludeMuted = filter.data.excludeMuted
|
||||
state.excludeArchived = filter.data.excludeArchived
|
||||
return state
|
||||
}
|
||||
})
|
||||
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
})
|
||||
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
},
|
||||
deleteIncludePeer: { peerId in
|
||||
updateState { state in
|
||||
|
@ -144,9 +144,9 @@ private enum ChatListRecentEntry: Comparable, Identifiable {
|
||||
if let user = primaryPeer as? TelegramUser {
|
||||
let servicePeer = isServicePeer(primaryPeer)
|
||||
if user.flags.contains(.isSupport) && !servicePeer {
|
||||
status = .custom(strings.Bot_GenericSupportStatus)
|
||||
status = .custom(string: strings.Bot_GenericSupportStatus, multiline: false)
|
||||
} else if let _ = user.botInfo {
|
||||
status = .custom(strings.Bot_GenericBotStatus)
|
||||
status = .custom(string: strings.Bot_GenericBotStatus, multiline: false)
|
||||
} else if user.id != context.account.peerId && !servicePeer {
|
||||
let presence = peer.presence ?? TelegramUserPresence(status: .none, lastActivity: 0)
|
||||
status = .presence(presence, timeFormat)
|
||||
@ -154,19 +154,19 @@ private enum ChatListRecentEntry: Comparable, Identifiable {
|
||||
status = .none
|
||||
}
|
||||
} else if let group = primaryPeer as? TelegramGroup {
|
||||
status = .custom(strings.GroupInfo_ParticipantCount(Int32(group.participantCount)))
|
||||
status = .custom(string: strings.GroupInfo_ParticipantCount(Int32(group.participantCount)), multiline: false)
|
||||
} else if let channel = primaryPeer as? TelegramChannel {
|
||||
if case .group = channel.info {
|
||||
if let count = peer.subpeerSummary?.count {
|
||||
status = .custom(strings.GroupInfo_ParticipantCount(Int32(count)))
|
||||
status = .custom(string: strings.GroupInfo_ParticipantCount(Int32(count)), multiline: false)
|
||||
} else {
|
||||
status = .custom(strings.Group_Status)
|
||||
status = .custom(string: strings.Group_Status, multiline: false)
|
||||
}
|
||||
} else {
|
||||
if let count = peer.subpeerSummary?.count {
|
||||
status = .custom(strings.Conversation_StatusSubscribers(Int32(count)))
|
||||
status = .custom(string: strings.Conversation_StatusSubscribers(Int32(count)), multiline: false)
|
||||
} else {
|
||||
status = .custom(strings.Channel_Status)
|
||||
status = .custom(string: strings.Channel_Status, multiline: false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -18,7 +18,7 @@ import ChatListSearchItemHeader
|
||||
|
||||
public enum ChatListNodeMode {
|
||||
case chatList
|
||||
case peers(filter: ChatListNodePeersFilter, isSelecting: Bool, additionalCategories: [ChatListNodeAdditionalCategory])
|
||||
case peers(filter: ChatListNodePeersFilter, isSelecting: Bool, additionalCategories: [ChatListNodeAdditionalCategory], chatListFilters: [ChatListFilter]?)
|
||||
}
|
||||
|
||||
struct ChatListNodeListViewTransition {
|
||||
@ -180,7 +180,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
switch mode {
|
||||
case .chatList:
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, context: context, peerGroupId: peerGroupId, filterData: filterData, index: index, content: .peer(message: message, peer: peer, combinedReadState: combinedReadState, isRemovedFromTotalUnreadCount: isRemovedFromTotalUnreadCount, presence: presence, summaryInfo: summaryInfo, embeddedState: embeddedState, inputActivities: inputActivities, promoInfo: promoInfo, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: hasFailedMessages), editing: editing, hasActiveRevealControls: hasActiveRevealControls, selected: selected, header: nil, enableContextActions: true, hiddenOffset: false, interaction: nodeInteraction), directionHint: entry.directionHint)
|
||||
case let .peers(filter, isSelecting, _):
|
||||
case let .peers(filter, isSelecting, _, filters):
|
||||
let itemPeer = peer.chatMainPeer
|
||||
var chatPeer: Peer?
|
||||
if let peer = peer.peers[peer.peerId] {
|
||||
@ -247,7 +247,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
|
||||
var header: ChatListSearchItemHeader?
|
||||
switch mode {
|
||||
case let .peers(_, _, additionalCategories):
|
||||
case let .peers(_, _, additionalCategories, _):
|
||||
if !additionalCategories.isEmpty {
|
||||
header = ChatListSearchItemHeader(type: .chats, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
|
||||
}
|
||||
@ -257,8 +257,11 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
|
||||
var status: ContactsPeerItemStatus = .none
|
||||
if isSelecting, let itemPeer = itemPeer {
|
||||
if let string = statusStringForPeerType(accountPeerId: context.account.peerId, strings: presentationData.strings, peer: itemPeer, isContact: isContact) {
|
||||
status = .custom(string)
|
||||
let tagSummaryCount = summaryInfo.tagSummaryCount ?? 0
|
||||
let actionsSummaryCount = summaryInfo.actionsSummaryCount ?? 0
|
||||
let totalMentionCount = tagSummaryCount - actionsSummaryCount
|
||||
if let (string, multiline) = statusStringForPeerType(accountPeerId: context.account.peerId, strings: presentationData.strings, peer: itemPeer, isMuted: isRemovedFromTotalUnreadCount, isUnread: combinedReadState?.isUnread ?? false, isContact: isContact, hasUnseenMentions: totalMentionCount > 0, chatListFilters: filters) {
|
||||
status = .custom(string: string, multiline: multiline)
|
||||
} else {
|
||||
status = .none
|
||||
}
|
||||
@ -291,7 +294,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
switch mode {
|
||||
case .chatList:
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(presentationData: presentationData, context: context, peerGroupId: peerGroupId, filterData: filterData, index: index, content: .peer(message: message, peer: peer, combinedReadState: combinedReadState, isRemovedFromTotalUnreadCount: isRemovedFromTotalUnreadCount, presence: presence, summaryInfo: summaryInfo, embeddedState: embeddedState, inputActivities: inputActivities, promoInfo: promoInfo, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: hasFailedMessages), editing: editing, hasActiveRevealControls: hasActiveRevealControls, selected: selected, header: nil, enableContextActions: true, hiddenOffset: false, interaction: nodeInteraction), directionHint: entry.directionHint)
|
||||
case let .peers(filter, isSelecting, _):
|
||||
case let .peers(filter, isSelecting, _, filters):
|
||||
let itemPeer = peer.chatMainPeer
|
||||
var chatPeer: Peer?
|
||||
if let peer = peer.peers[peer.peerId] {
|
||||
@ -314,7 +317,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
}
|
||||
var header: ChatListSearchItemHeader?
|
||||
switch mode {
|
||||
case let .peers(_, _, additionalCategories):
|
||||
case let .peers(_, _, additionalCategories, _):
|
||||
if !additionalCategories.isEmpty {
|
||||
header = ChatListSearchItemHeader(type: .chats, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
|
||||
}
|
||||
@ -324,8 +327,11 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
|
||||
var status: ContactsPeerItemStatus = .none
|
||||
if isSelecting, let itemPeer = itemPeer {
|
||||
if let string = statusStringForPeerType(accountPeerId: context.account.peerId, strings: presentationData.strings, peer: itemPeer, isContact: isContact) {
|
||||
status = .custom(string)
|
||||
let tagSummaryCount = summaryInfo.tagSummaryCount ?? 0
|
||||
let actionsSummaryCount = summaryInfo.actionsSummaryCount ?? 0
|
||||
let totalMentionCount = tagSummaryCount - actionsSummaryCount
|
||||
if let (string, multiline) = statusStringForPeerType(accountPeerId: context.account.peerId, strings: presentationData.strings, peer: itemPeer, isMuted: isRemovedFromTotalUnreadCount, isUnread: combinedReadState?.isUnread ?? false, isContact: isContact, hasUnseenMentions: totalMentionCount > 0, chatListFilters: filters) {
|
||||
status = .custom(string: string, multiline: multiline)
|
||||
} else {
|
||||
status = .none
|
||||
}
|
||||
@ -514,7 +520,7 @@ public final class ChatListNode: ListView {
|
||||
self.mode = mode
|
||||
|
||||
var isSelecting = false
|
||||
if case .peers(_, true, _) = mode {
|
||||
if case .peers(_, true, _, _) = mode {
|
||||
isSelecting = true
|
||||
}
|
||||
|
||||
@ -674,7 +680,7 @@ public final class ChatListNode: ListView {
|
||||
let currentRemovingPeerId = self.currentRemovingPeerId
|
||||
|
||||
let savedMessagesPeer: Signal<Peer?, NoError>
|
||||
if case let .peers(filter, _, _) = mode, filter.contains(.onlyWriteable) {
|
||||
if case let .peers(filter, _, _, _) = mode, filter.contains(.onlyWriteable) {
|
||||
savedMessagesPeer = context.account.postbox.loadedPeerWithId(context.account.peerId)
|
||||
|> map(Optional.init)
|
||||
} else {
|
||||
@ -727,7 +733,7 @@ public final class ChatListNode: ListView {
|
||||
switch mode {
|
||||
case .chatList:
|
||||
return true
|
||||
case let .peers(filter, _, _):
|
||||
case let .peers(filter, _, _, _):
|
||||
guard !filter.contains(.excludeSavedMessages) || peer.peerId != currentPeerId else { return false }
|
||||
guard !filter.contains(.excludeSecretChats) || peer.peerId.namespace != Namespaces.Peer.SecretChat else { return false }
|
||||
guard !filter.contains(.onlyPrivateChats) || peer.peerId.namespace == Namespaces.Peer.CloudUser else { return false }
|
||||
@ -1807,32 +1813,52 @@ public final class ChatListNode: ListView {
|
||||
}
|
||||
}
|
||||
|
||||
private func statusStringForPeerType(accountPeerId: PeerId, strings: PresentationStrings, peer: Peer, isContact: Bool) -> String? {
|
||||
private func statusStringForPeerType(accountPeerId: PeerId, strings: PresentationStrings, peer: Peer, isMuted: Bool, isUnread: Bool, isContact: Bool, hasUnseenMentions: Bool, chatListFilters: [ChatListFilter]?) -> (String, Bool)? {
|
||||
if accountPeerId == peer.id {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let chatListFilters = chatListFilters {
|
||||
var result = ""
|
||||
for filter in chatListFilters {
|
||||
let predicate = chatListFilterPredicate(filter: filter.data)
|
||||
if predicate.includes(peer: peer, groupId: .root, isRemovedFromTotalUnreadCount: isMuted, isUnread: isUnread, isContact: isContact, messageTagSummaryResult: hasUnseenMentions) {
|
||||
if !result.isEmpty {
|
||||
result.append(", ")
|
||||
}
|
||||
result.append(filter.title)
|
||||
}
|
||||
}
|
||||
|
||||
if result.isEmpty {
|
||||
return nil
|
||||
} else {
|
||||
return (result, true)
|
||||
}
|
||||
}
|
||||
|
||||
if let user = peer as? TelegramUser {
|
||||
if user.botInfo != nil || user.flags.contains(.isSupport) {
|
||||
return strings.ChatList_PeerTypeBot
|
||||
return (strings.ChatList_PeerTypeBot, false)
|
||||
} else if isContact {
|
||||
return strings.ChatList_PeerTypeContact
|
||||
return (strings.ChatList_PeerTypeContact, false)
|
||||
} else {
|
||||
return strings.ChatList_PeerTypeNonContact
|
||||
return (strings.ChatList_PeerTypeNonContact, false)
|
||||
}
|
||||
} else if peer is TelegramSecretChat {
|
||||
if isContact {
|
||||
return strings.ChatList_PeerTypeContact
|
||||
return (strings.ChatList_PeerTypeContact, false)
|
||||
} else {
|
||||
return strings.ChatList_PeerTypeNonContact
|
||||
return (strings.ChatList_PeerTypeNonContact, false)
|
||||
}
|
||||
} else if peer is TelegramGroup {
|
||||
return strings.ChatList_PeerTypeGroup
|
||||
return (strings.ChatList_PeerTypeGroup, false)
|
||||
} else if let channel = peer as? TelegramChannel {
|
||||
if case .group = channel.info {
|
||||
return strings.ChatList_PeerTypeGroup
|
||||
return (strings.ChatList_PeerTypeGroup, false)
|
||||
} else {
|
||||
return strings.ChatList_PeerTypeChannel
|
||||
return (strings.ChatList_PeerTypeChannel, false)
|
||||
}
|
||||
}
|
||||
return strings.ChatList_PeerTypeNonContact
|
||||
return (strings.ChatList_PeerTypeNonContact, false)
|
||||
}
|
||||
|
@ -364,7 +364,8 @@ func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState,
|
||||
result.append(.HeaderEntry)
|
||||
}
|
||||
|
||||
if view.laterIndex == nil, case let .peers(_, _, additionalCategories) = mode {
|
||||
if view.laterIndex == nil, case let .peers(_, _, additionalCategories,
|
||||
_) = mode {
|
||||
var index = 0
|
||||
for category in additionalCategories.reversed(){
|
||||
result.append(.AdditionalCategory(index: index, id: category.id, title: category.title, image: category.icon, selected: state.selectedAdditionalCategoryIds.contains(category.id), presentationData: state.presentationData))
|
||||
|
@ -190,19 +190,19 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
|
||||
let presence = presence ?? TelegramUserPresence(status: .none, lastActivity: 0)
|
||||
status = .presence(presence, dateTimeFormat)
|
||||
} else if let group = peer as? TelegramGroup {
|
||||
status = .custom(strings.Conversation_StatusMembers(Int32(group.participantCount)))
|
||||
status = .custom(string: strings.Conversation_StatusMembers(Int32(group.participantCount)), multiline: false)
|
||||
} else if let channel = peer as? TelegramChannel {
|
||||
if case .group = channel.info {
|
||||
if let participantCount = participantCount, participantCount != 0 {
|
||||
status = .custom(strings.Conversation_StatusMembers(participantCount))
|
||||
status = .custom(string: strings.Conversation_StatusMembers(participantCount), multiline: false)
|
||||
} else {
|
||||
status = .custom(strings.Group_Status)
|
||||
status = .custom(string: strings.Group_Status, multiline: false)
|
||||
}
|
||||
} else {
|
||||
if let participantCount = participantCount, participantCount != 0 {
|
||||
status = .custom(strings.Conversation_StatusSubscribers(participantCount))
|
||||
status = .custom(string: strings.Conversation_StatusSubscribers(participantCount), multiline: false)
|
||||
} else {
|
||||
status = .custom(strings.Channel_Status)
|
||||
status = .custom(string: strings.Channel_Status, multiline: false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -54,7 +54,7 @@ private enum InviteContactsEntry: Comparable, Identifiable {
|
||||
case let .peer(_, id, contact, count, selection, theme, strings, nameSortOrder, nameDisplayOrder):
|
||||
let status: ContactsPeerItemStatus
|
||||
if count != 0 {
|
||||
status = .custom(strings.Contacts_ImportersCount(count))
|
||||
status = .custom(string: strings.Contacts_ImportersCount(count), multiline: false)
|
||||
} else {
|
||||
status = .none
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ public enum ContactsPeerItemStatus {
|
||||
case none
|
||||
case presence(PeerPresence, PresentationDateTimeFormat)
|
||||
case addressName(String)
|
||||
case custom(String)
|
||||
case custom(string: String, multiline: Bool)
|
||||
}
|
||||
|
||||
public enum ContactsPeerItemSelection: Equatable {
|
||||
@ -499,6 +499,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
|
||||
var titleAttributedString: NSAttributedString?
|
||||
var statusAttributedString: NSAttributedString?
|
||||
var multilineStatus: Bool = false
|
||||
var userPresence: TelegramUserPresence?
|
||||
|
||||
switch item.peer {
|
||||
@ -563,8 +564,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
} else if !suffix.isEmpty {
|
||||
statusAttributedString = NSAttributedString(string: suffix, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
|
||||
}
|
||||
case let .custom(text):
|
||||
case let .custom(text, multiline):
|
||||
statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
|
||||
multilineStatus = multiline
|
||||
}
|
||||
}
|
||||
case let .deviceContact(_, contact):
|
||||
@ -585,8 +587,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
|
||||
switch item.status {
|
||||
case let .custom(text):
|
||||
case let .custom(text, multiline):
|
||||
statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
|
||||
multilineStatus = multiline
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -625,7 +628,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - rightInset - additionalTitleInset), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - rightInset - badgeSize), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: multilineStatus ? 3 : 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - rightInset - badgeSize), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let titleVerticalInset: CGFloat = statusAttributedString == nil ? 13.0 : 6.0
|
||||
let verticalInset: CGFloat = statusAttributedString == nil ? 13.0 : 6.0
|
||||
|
@ -392,6 +392,7 @@ private final class InnerTextSelectionTipContainerNode: ASDisplayNode {
|
||||
final class ContextActionsContainerNode: ASDisplayNode {
|
||||
private let actionsNode: InnerActionsContainerNode
|
||||
private let textSelectionTipNode: InnerTextSelectionTipContainerNode?
|
||||
private let scrollNode: ASScrollNode
|
||||
|
||||
var panSelectionGestureEnabled: Bool = true {
|
||||
didSet {
|
||||
@ -411,10 +412,19 @@ final class ContextActionsContainerNode: ASDisplayNode {
|
||||
self.textSelectionTipNode = nil
|
||||
}
|
||||
|
||||
self.scrollNode = ASScrollNode()
|
||||
self.scrollNode.canCancelAllTouchesInViews = true
|
||||
self.scrollNode.view.delaysContentTouches = false
|
||||
self.scrollNode.view.showsVerticalScrollIndicator = false
|
||||
if #available(iOS 11.0, *) {
|
||||
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.actionsNode)
|
||||
self.textSelectionTipNode.flatMap(self.addSubnode)
|
||||
self.scrollNode.addSubnode(self.actionsNode)
|
||||
self.textSelectionTipNode.flatMap(self.scrollNode.addSubnode)
|
||||
self.addSubnode(self.scrollNode)
|
||||
}
|
||||
|
||||
func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
@ -433,6 +443,11 @@ final class ContextActionsContainerNode: ASDisplayNode {
|
||||
return contentSize
|
||||
}
|
||||
|
||||
func updateSize(containerSize: CGSize, contentSize: CGSize) {
|
||||
self.scrollNode.view.contentSize = contentSize
|
||||
self.scrollNode.frame = CGRect(origin: CGPoint(), size: containerSize)
|
||||
}
|
||||
|
||||
func actionNode(at point: CGPoint) -> ContextActionNode? {
|
||||
return self.actionsNode.actionNode(at: self.view.convert(point, to: self.actionsNode.view))
|
||||
}
|
||||
|
@ -1111,6 +1111,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view)
|
||||
|
||||
let actionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, constrainedWidth: layout.size.width - actionsSideInset * 2.0, transition: actionsContainerTransition)
|
||||
self.actionsContainerNode.updateSize(containerSize: actionsSize, contentSize: actionsSize)
|
||||
let contentSize = originalProjectedContentViewFrame.1.size
|
||||
self.contentContainerNode.updateLayout(size: contentSize, scaledSize: contentSize, transition: transition)
|
||||
|
||||
@ -1237,11 +1238,16 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
let contentScale = (constrainedWidth - actionsSideInset * 2.0) / constrainedWidth
|
||||
var contentUnscaledSize: CGSize
|
||||
if case .compact = layout.metrics.widthClass {
|
||||
self.actionsContainerNode.updateSize(containerSize: actionsSize, contentSize: actionsSize)
|
||||
|
||||
let proposedContentHeight: CGFloat
|
||||
if layout.size.width < layout.size.height {
|
||||
proposedContentHeight = layout.size.height - topEdge - contentActionsSpacing - actionsSize.height - layout.intrinsicInsets.bottom - actionsBottomInset
|
||||
} else {
|
||||
proposedContentHeight = layout.size.height - topEdge - topEdge
|
||||
|
||||
let maxActionsHeight = layout.size.height - topEdge - topEdge
|
||||
self.actionsContainerNode.updateSize(containerSize: CGSize(width: actionsSize.width, height: min(actionsSize.height, maxActionsHeight)), contentSize: actionsSize)
|
||||
}
|
||||
contentUnscaledSize = CGSize(width: constrainedWidth, height: max(100.0, proposedContentHeight))
|
||||
|
||||
@ -1249,6 +1255,9 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
contentUnscaledSize = preferredSize
|
||||
}
|
||||
} else {
|
||||
let maxActionsHeight = layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - actionsSize.height
|
||||
self.actionsContainerNode.updateSize(containerSize: CGSize(width: actionsSize.width, height: min(actionsSize.height, maxActionsHeight)), contentSize: actionsSize)
|
||||
|
||||
let proposedContentHeight = layout.size.height - topEdge - contentActionsSpacing - actionsSize.height - layout.intrinsicInsets.bottom - actionsBottomInset
|
||||
contentUnscaledSize = CGSize(width: min(layout.size.width, 340.0), height: min(568.0, proposedContentHeight))
|
||||
|
||||
|
@ -71,6 +71,22 @@ public final class ConstantDisplayLinkAnimator {
|
||||
private let update: () -> Void
|
||||
private var completed = false
|
||||
|
||||
public var frameInterval: Int = 1 {
|
||||
didSet {
|
||||
if #available(iOS 10.0, *) {
|
||||
let preferredFramesPerSecond: Int
|
||||
if self.frameInterval == 1 {
|
||||
preferredFramesPerSecond = 60
|
||||
} else {
|
||||
preferredFramesPerSecond = 30
|
||||
}
|
||||
self.displayLink.preferredFramesPerSecond = preferredFramesPerSecond
|
||||
} else {
|
||||
self.displayLink.frameInterval = self.frameInterval
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var isPaused: Bool = true {
|
||||
didSet {
|
||||
if self.isPaused != oldValue {
|
||||
|
@ -22,7 +22,9 @@ private func tagsForMessage(_ message: Message) -> MessageTags? {
|
||||
return .photoOrVideo
|
||||
case let file as TelegramMediaFile:
|
||||
if file.isVideo {
|
||||
if !file.isAnimated {
|
||||
if file.isAnimated {
|
||||
return .gif
|
||||
} else {
|
||||
return .photoOrVideo
|
||||
}
|
||||
} else if file.isVoice {
|
||||
|
@ -148,7 +148,7 @@ private final class ChannelMembersSearchEntry: Comparable, Identifiable {
|
||||
case let .participant(participant, label, revealActions, revealed, enabled):
|
||||
let status: ContactsPeerItemStatus
|
||||
if let label = label {
|
||||
status = .custom(label)
|
||||
status = .custom(string: label, multiline: false)
|
||||
} else if let presence = participant.presences[participant.peer.id], self.addIcon {
|
||||
status = .presence(presence, dateTimeFormat)
|
||||
} else {
|
||||
|
@ -65,7 +65,7 @@ private enum ChannelMembersSearchEntry: Comparable, Identifiable {
|
||||
case let .peer(_, participant, editing, label, enabled):
|
||||
let status: ContactsPeerItemStatus
|
||||
if let label = label {
|
||||
status = .custom(label)
|
||||
status = .custom(string: label, multiline: false)
|
||||
} else {
|
||||
status = .none
|
||||
}
|
||||
|
@ -173,7 +173,7 @@ private enum OldChannelsEntry: ItemListNodeEntry {
|
||||
case let .peersHeader(title):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
|
||||
case let .peer(_, peer, selected):
|
||||
return ContactsPeerItem(presentationData: presentationData, style: .blocks, sectionId: self.section, sortOrder: .firstLast, displayOrder: .firstLast, context: arguments.context, peerMode: .peer, peer: .peer(peer: peer.peer, chatPeer: peer.peer), status: .custom(localizedOldChannelDate(peer: peer, strings: presentationData.strings)), badge: nil, enabled: true, selection: ContactsPeerItemSelection.selectable(selected: selected), editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), options: [], actionIcon: .none, index: nil, header: nil, action: { _ in
|
||||
return ContactsPeerItem(presentationData: presentationData, style: .blocks, sectionId: self.section, sortOrder: .firstLast, displayOrder: .firstLast, context: arguments.context, peerMode: .peer, peer: .peer(peer: peer.peer, chatPeer: peer.peer), status: .custom(string: localizedOldChannelDate(peer: peer, strings: presentationData.strings), multiline: false), badge: nil, enabled: true, selection: ContactsPeerItemSelection.selectable(selected: selected), editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), options: [], actionIcon: .none, index: nil, header: nil, action: { _ in
|
||||
arguments.togglePeer(peer.peer.id, true)
|
||||
}, setPeerIdWithRevealedOptions: nil, deletePeer: nil, itemHighlighting: nil, contextAction: nil)
|
||||
}
|
||||
|
@ -142,7 +142,7 @@ private enum OldChannelsSearchEntry: Comparable, Identifiable {
|
||||
func item(context: AccountContext, presentationData: ItemListPresentationData, interaction: OldChannelsSearchInteraction) -> ListViewItem {
|
||||
switch self {
|
||||
case let .peer(_, peer, selected):
|
||||
return ContactsPeerItem(presentationData: presentationData, style: .plain, sortOrder: .firstLast, displayOrder: .firstLast, context: context, peerMode: .peer, peer: .peer(peer: peer.peer, chatPeer: peer.peer), status: .custom(localizedOldChannelDate(peer: peer, strings: presentationData.strings)), badge: nil, enabled: true, selection: ContactsPeerItemSelection.selectable(selected: selected), editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), options: [], actionIcon: .none, index: nil, header: nil, action: { _ in
|
||||
return ContactsPeerItem(presentationData: presentationData, style: .plain, sortOrder: .firstLast, displayOrder: .firstLast, context: context, peerMode: .peer, peer: .peer(peer: peer.peer, chatPeer: peer.peer), status: .custom(string: localizedOldChannelDate(peer: peer, strings: presentationData.strings), multiline: false), badge: nil, enabled: true, selection: ContactsPeerItemSelection.selectable(selected: selected), editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), options: [], actionIcon: .none, index: nil, header: nil, action: { _ in
|
||||
interaction.togglePeer(peer.peer.id)
|
||||
}, setPeerIdWithRevealedOptions: nil, deletePeer: nil, itemHighlighting: nil, contextAction: nil)
|
||||
}
|
||||
|
@ -267,7 +267,7 @@ public struct ChatListFilterPredicate {
|
||||
self.include = include
|
||||
}
|
||||
|
||||
func includes(peer: Peer, groupId: PeerGroupId, isRemovedFromTotalUnreadCount: Bool, isUnread: Bool, isContact: Bool, messageTagSummaryResult: Bool?) -> Bool {
|
||||
public func includes(peer: Peer, groupId: PeerGroupId, isRemovedFromTotalUnreadCount: Bool, isUnread: Bool, isContact: Bool, messageTagSummaryResult: Bool?) -> Bool {
|
||||
if self.pinnedPeerIds.contains(peer.id) {
|
||||
return false
|
||||
}
|
||||
|
@ -93,8 +93,9 @@ public extension MessageTags {
|
||||
static let voiceOrInstantVideo = MessageTags(rawValue: 1 << 4)
|
||||
static let unseenPersonalMessage = MessageTags(rawValue: 1 << 5)
|
||||
static let liveLocation = MessageTags(rawValue: 1 << 6)
|
||||
static let gif = MessageTags(rawValue: 1 << 7)
|
||||
|
||||
static let all: MessageTags = [.photoOrVideo, .file, .music, .webPage, .voiceOrInstantVideo, .unseenPersonalMessage, .liveLocation]
|
||||
static let all: MessageTags = [.photoOrVideo, .file, .music, .webPage, .voiceOrInstantVideo, .unseenPersonalMessage, .liveLocation, .gif]
|
||||
}
|
||||
|
||||
public extension GlobalMessageTags {
|
||||
|
@ -140,6 +140,21 @@ public struct ChatListFilterIncludePeers: Equatable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public mutating func addPeer(_ peerId: PeerId) -> Bool {
|
||||
if self.pinnedPeers.contains(peerId) {
|
||||
return false
|
||||
}
|
||||
if self.peers.contains(peerId) {
|
||||
return false
|
||||
}
|
||||
|
||||
if self.peers.count + self.pinnedPeers.count >= 100 {
|
||||
return false
|
||||
}
|
||||
self.peers.insert(peerId, at: 0)
|
||||
return true
|
||||
}
|
||||
|
||||
public mutating func setPeers(_ peers: [PeerId]) {
|
||||
self.peers = peers
|
||||
self.pinnedPeers = self.pinnedPeers.filter { peers.contains($0) }
|
||||
@ -176,6 +191,18 @@ public struct ChatListFilterData: Equatable, Hashable {
|
||||
self.includePeers = includePeers
|
||||
self.excludePeers = excludePeers
|
||||
}
|
||||
|
||||
public mutating func addIncludePeer(peerId: PeerId) -> Bool {
|
||||
if self.includePeers.peers.contains(peerId) || self.includePeers.pinnedPeers.contains(peerId) {
|
||||
return false
|
||||
}
|
||||
if self.includePeers.addPeer(peerId) {
|
||||
self.excludePeers.removeAll(where: { $0 == peerId })
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct ChatListFilter: PostboxCoding, Equatable {
|
||||
|
@ -462,7 +462,17 @@ func revalidateMediaResourceReference(postbox: Postbox, network: Network, revali
|
||||
if let partialReference = file.partialReference {
|
||||
updatedReference = partialReference.mediaReference(media.media).resourceReference(resource)
|
||||
}
|
||||
if file.isSticker, messageReference.isSecret == true {
|
||||
|
||||
var revalidateWithStickerpack = false
|
||||
if file.isSticker {
|
||||
if messageReference.isSecret == true {
|
||||
revalidateWithStickerpack = true
|
||||
} else if case .none = messageReference.content {
|
||||
revalidateWithStickerpack = true
|
||||
}
|
||||
}
|
||||
|
||||
if revalidateWithStickerpack {
|
||||
var stickerPackReference: StickerPackReference?
|
||||
for attribute in file.attributes {
|
||||
if case let .Sticker(sticker) = attribute {
|
||||
@ -503,7 +513,13 @@ func revalidateMediaResourceReference(postbox: Postbox, network: Network, revali
|
||||
if let item = item as? StickerPackItem {
|
||||
if media.id != nil && item.file.id == media.id {
|
||||
if let updatedResource = findUpdatedMediaResource(media: item.file, previousMedia: media, resource: resource) {
|
||||
return .single(RevalidatedMediaResource(updatedResource: updatedResource, updatedReference: nil))
|
||||
return postbox.transaction { transaction -> RevalidatedMediaResource in
|
||||
if let id = media.id {
|
||||
updateMessageMedia(transaction: transaction, id: id, media: item.file)
|
||||
}
|
||||
return RevalidatedMediaResource(updatedResource: updatedResource, updatedReference: nil)
|
||||
}
|
||||
|> castError(RevalidateMediaReferenceError.self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,8 @@ func messageFilterForTagMask(_ tagMask: MessageTags) -> Api.MessagesFilter? {
|
||||
return Api.MessagesFilter.inputMessagesFilterUrl
|
||||
} else if tagMask == .voiceOrInstantVideo {
|
||||
return Api.MessagesFilter.inputMessagesFilterRoundVoice
|
||||
} else if tagMask == .gif {
|
||||
return Api.MessagesFilter.inputMessagesFilterGif
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
@ -271,7 +271,17 @@ private enum MultipartFetchSource {
|
||||
case master(location: MultipartFetchMasterLocation, download: DownloadWrapper)
|
||||
case cdn(masterDatacenterId: Int32, fileToken: Data, key: Data, iv: Data, download: DownloadWrapper, masterDownload: DownloadWrapper, hashSource: MultipartCdnHashSource)
|
||||
|
||||
func request(offset: Int32, limit: Int32, tag: MediaResourceFetchTag?, resource: TelegramMediaResource, resourceReference: MediaResourceReference?, fileReference: Data?, continueInBackground: Bool) -> Signal<Data, MultipartFetchDownloadError> {
|
||||
func request(offset: Int32, limit: Int32, tag: MediaResourceFetchTag?, resource: TelegramMediaResource, resourceReference: FetchResourceReference, fileReference: Data?, continueInBackground: Bool) -> Signal<Data, MultipartFetchDownloadError> {
|
||||
var resourceReferenceValue: MediaResourceReference?
|
||||
switch resourceReference {
|
||||
case .forceRevalidate:
|
||||
return .fail(.revalidateMediaReference)
|
||||
case .empty:
|
||||
resourceReferenceValue = nil
|
||||
case let .reference(value):
|
||||
resourceReferenceValue = value
|
||||
}
|
||||
|
||||
switch self {
|
||||
case .none:
|
||||
return .never()
|
||||
@ -281,7 +291,7 @@ private enum MultipartFetchSource {
|
||||
|
||||
switch location {
|
||||
case let .generic(_, location):
|
||||
switch location(resource, resourceReference, fileReference) {
|
||||
switch location(resource, resourceReferenceValue, fileReference) {
|
||||
case .none:
|
||||
return .fail(.revalidateMediaReference)
|
||||
case .revalidate:
|
||||
@ -382,13 +392,19 @@ private enum MultipartFetchSource {
|
||||
}
|
||||
}
|
||||
|
||||
private enum FetchResourceReference {
|
||||
case empty
|
||||
case forceRevalidate
|
||||
case reference(MediaResourceReference)
|
||||
}
|
||||
|
||||
private final class MultipartFetchManager {
|
||||
let parallelParts: Int
|
||||
let defaultPartSize = 128 * 1024
|
||||
var partAlignment = 4 * 1024
|
||||
|
||||
var resource: TelegramMediaResource
|
||||
var resourceReference: MediaResourceReference?
|
||||
var resourceReference: FetchResourceReference
|
||||
var fileReference: Data?
|
||||
let parameters: MediaResourceFetchParameters?
|
||||
let consumerId: Int64
|
||||
@ -441,10 +457,30 @@ private final class MultipartFetchManager {
|
||||
if let info = parameters?.info as? TelegramCloudMediaResourceFetchInfo {
|
||||
self.fileReference = info.reference.apiFileReference
|
||||
self.continueInBackground = info.continueInBackground
|
||||
self.resourceReference = info.reference
|
||||
self.resourceReference = .reference(info.reference)
|
||||
switch info.reference {
|
||||
case let .media(media, _):
|
||||
if let file = media.media as? TelegramMediaFile {
|
||||
for attribute in file.attributes {
|
||||
switch attribute {
|
||||
case let .Sticker(_, packReference, _):
|
||||
switch packReference {
|
||||
case .name:
|
||||
self.resourceReference = .forceRevalidate
|
||||
default:
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
} else {
|
||||
self.continueInBackground = false
|
||||
self.resourceReference = nil
|
||||
self.resourceReference = .empty
|
||||
}
|
||||
|
||||
self.state = MultipartDownloadState(encryptionKey: encryptionKey, decryptedSize: decryptedSize)
|
||||
@ -611,7 +647,6 @@ private final class MultipartFetchManager {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
var data = data
|
||||
if data.count < downloadRange.count {
|
||||
strongSelf.completeSize = downloadRange.lowerBound + data.count
|
||||
}
|
||||
@ -639,7 +674,11 @@ private final class MultipartFetchManager {
|
||||
strongSelf.fileReference = reference
|
||||
}
|
||||
strongSelf.resource = validationResult.updatedResource
|
||||
strongSelf.resourceReference = validationResult.updatedReference
|
||||
if let reference = validationResult.updatedReference {
|
||||
strongSelf.resourceReference = .reference(reference)
|
||||
} else {
|
||||
strongSelf.resourceReference = .empty
|
||||
}
|
||||
strongSelf.checkState()
|
||||
}
|
||||
}, error: { _ in
|
||||
|
@ -63,7 +63,7 @@ public func tagsForStoreMessage(incoming: Bool, attributes: [MessageAttribute],
|
||||
}
|
||||
}
|
||||
if isAnimated {
|
||||
refinedTag = nil
|
||||
refinedTag = .gif
|
||||
}
|
||||
if file.isAnimatedSticker {
|
||||
refinedTag = nil
|
||||
|
File diff suppressed because it is too large
Load Diff
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Back.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Back.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_menuback.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Back.imageset/ic_menuback.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Back.imageset/ic_menuback.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Folder.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Folder.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ic_addtofolder.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Folder.imageset/ic_addtofolder.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Folder.imageset/ic_addtofolder.pdf
vendored
Normal file
Binary file not shown.
Binary file not shown.
@ -125,7 +125,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
|
||||
})
|
||||
|
||||
switch self.mode {
|
||||
case let .chatSelection(_, selectedChats, additionalCategories):
|
||||
case let .chatSelection(_, selectedChats, additionalCategories, _):
|
||||
let _ = (self.context.account.postbox.transaction { transaction -> [Peer] in
|
||||
return selectedChats.compactMap(transaction.getPeer)
|
||||
}
|
||||
@ -425,7 +425,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
|
||||
break
|
||||
case let .chats(chatsNode):
|
||||
var categoryToken: EditableTokenListToken?
|
||||
if case let .chatSelection(_, _, additionalCategories) = strongSelf.mode {
|
||||
if case let .chatSelection(_, _, additionalCategories, _) = strongSelf.mode {
|
||||
if let additionalCategories = additionalCategories {
|
||||
for i in 0 ..< additionalCategories.categories.count {
|
||||
if additionalCategories.categories[i].id == id {
|
||||
|
@ -84,9 +84,9 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
|
||||
placeholder = self.presentationData.strings.Compose_TokenListPlaceholder
|
||||
}
|
||||
|
||||
if case let .chatSelection(_, selectedChats, additionalCategories) = mode {
|
||||
if case let .chatSelection(_, selectedChats, additionalCategories, chatListFilters) = mode {
|
||||
placeholder = self.presentationData.strings.ChatListFilter_AddChatsTitle
|
||||
let chatListNode = ChatListNode(context: context, groupId: .root, previewing: false, fillPreloadItems: false, mode: .peers(filter: [.excludeSecretChats], isSelecting: true, additionalCategories: additionalCategories?.categories ?? []), theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true)
|
||||
let chatListNode = ChatListNode(context: context, groupId: .root, previewing: false, fillPreloadItems: false, mode: .peers(filter: [.excludeSecretChats], isSelecting: true, additionalCategories: additionalCategories?.categories ?? [], chatListFilters: chatListFilters), theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true)
|
||||
chatListNode.updateState { state in
|
||||
var state = state
|
||||
for peerId in selectedChats {
|
||||
|
@ -38,6 +38,11 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
||||
private let context: AccountContext
|
||||
private let interaction: VisualMediaItemInteraction
|
||||
|
||||
private var videoLayerFrameManager: SoftwareVideoLayerFrameManager?
|
||||
private var sampleBufferLayer: SampleBufferLayer?
|
||||
private var displayLink: ConstantDisplayLinkAnimator?
|
||||
private var displayLinkTimestamp: Double = 0.0
|
||||
|
||||
private let containerNode: ContextControllerSourceNode
|
||||
private let imageNode: TransformImageNode
|
||||
private var statusNode: RadialStatusNode
|
||||
@ -179,6 +184,21 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
if let file = media as? TelegramMediaFile, file.isAnimated {
|
||||
let sampleBufferLayer: SampleBufferLayer
|
||||
if let current = self.sampleBufferLayer {
|
||||
sampleBufferLayer = current
|
||||
} else {
|
||||
sampleBufferLayer = takeSampleBufferLayer()
|
||||
self.sampleBufferLayer = sampleBufferLayer
|
||||
self.containerNode.layer.insertSublayer(sampleBufferLayer.layer, above: self.imageNode.layer)
|
||||
}
|
||||
self.videoLayerFrameManager = SoftwareVideoLayerFrameManager(account: self.context.account, fileReference: FileMediaReference.message(message: MessageReference(item.message), media: file), resource: file.resource, layerHolder: sampleBufferLayer)
|
||||
self.videoLayerFrameManager?.start()
|
||||
} else {
|
||||
self.videoLayerFrameManager = nil
|
||||
}
|
||||
|
||||
if let media = media, (self.item?.1 == nil || !media.isEqual(to: self.item!.1!)) {
|
||||
var mediaDimensions: CGSize?
|
||||
if let image = media as? TelegramMediaImage, let largestSize = largestImageRepresentation(image.representations)?.dimensions {
|
||||
@ -196,7 +216,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
||||
mediaDimensions = file.dimensions?.cgSize
|
||||
self.imageNode.setSignal(mediaGridMessageVideo(postbox: context.account.postbox, videoReference: .message(message: MessageReference(item.message), media: file), synchronousLoad: synchronousLoad, autoFetchFullSizeThumbnail: true), attemptSynchronously: synchronousLoad)
|
||||
|
||||
self.mediaBadgeNode.isHidden = false
|
||||
self.mediaBadgeNode.isHidden = file.isAnimated
|
||||
|
||||
self.resourceStatus = nil
|
||||
|
||||
@ -290,6 +310,9 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
||||
|
||||
self.containerNode.frame = imageFrame
|
||||
self.imageNode.frame = imageFrame
|
||||
if let sampleBufferLayer = self.sampleBufferLayer {
|
||||
sampleBufferLayer.layer.frame = imageFrame
|
||||
}
|
||||
|
||||
if let mediaDimensions = mediaDimensions {
|
||||
let imageSize = mediaDimensions.aspectFilled(imageFrame.size)
|
||||
@ -300,8 +323,28 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
func updateIsVisible(_ isVisible: Bool) {
|
||||
if let _ = self.videoLayerFrameManager {
|
||||
let displayLink: ConstantDisplayLinkAnimator
|
||||
if let current = self.displayLink {
|
||||
displayLink = current
|
||||
} else {
|
||||
displayLink = ConstantDisplayLinkAnimator { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.videoLayerFrameManager?.tick(timestamp: strongSelf.displayLinkTimestamp)
|
||||
strongSelf.displayLinkTimestamp += 1.0 / 30.0
|
||||
}
|
||||
displayLink.frameInterval = 2
|
||||
self.displayLink = displayLink
|
||||
}
|
||||
displayLink.isPaused = !isVisible
|
||||
}
|
||||
}
|
||||
|
||||
func updateSelectionState(animated: Bool) {
|
||||
if let (item, media, _, mediaDimensions) = self.item, let theme = self.theme {
|
||||
if let (item, _, _, _) = self.item, let theme = self.theme {
|
||||
self.containerNode.isGestureEnabled = self.interaction.selectedMessageIds == nil
|
||||
|
||||
if let selectedIds = self.interaction.selectedMessageIds {
|
||||
@ -379,9 +422,20 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
||||
|
||||
private final class VisualMediaItem {
|
||||
let message: Message
|
||||
let aspectRatio: CGFloat
|
||||
|
||||
init(message: Message) {
|
||||
self.message = message
|
||||
|
||||
var aspectRatio: CGFloat = 1.0
|
||||
for media in message.media {
|
||||
if let file = media as? TelegramMediaFile {
|
||||
if let dimensions = file.dimensions, dimensions.height > 1 {
|
||||
aspectRatio = CGFloat(dimensions.width) / CGFloat(dimensions.height)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.aspectRatio = aspectRatio
|
||||
}
|
||||
}
|
||||
|
||||
@ -434,10 +488,137 @@ private final class FloatingHeaderNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
private func tagMaskForType(_ type: PeerInfoVisualMediaPaneNode.ContentType) -> MessageTags {
|
||||
switch type {
|
||||
case .photoOrVideo:
|
||||
return .photoOrVideo
|
||||
case .gifs:
|
||||
return .gif
|
||||
}
|
||||
}
|
||||
|
||||
private enum ItemsLayout {
|
||||
final class Grid {
|
||||
let containerWidth: CGFloat
|
||||
let itemCount: Int
|
||||
let itemSpacing: CGFloat
|
||||
let itemsInRow: Int
|
||||
let itemSize: CGFloat
|
||||
let rowCount: Int
|
||||
let contentHeight: CGFloat
|
||||
|
||||
init(containerWidth: CGFloat, itemCount: Int, bottomInset: CGFloat) {
|
||||
self.containerWidth = containerWidth
|
||||
self.itemCount = itemCount
|
||||
self.itemSpacing = 1.0
|
||||
self.itemsInRow = max(3, min(6, Int(containerWidth / 140.0)))
|
||||
self.itemSize = floor(containerWidth / CGFloat(itemsInRow))
|
||||
|
||||
self.rowCount = itemCount / self.itemsInRow + (itemCount % self.itemsInRow == 0 ? 0 : 1)
|
||||
|
||||
self.contentHeight = CGFloat(self.rowCount + 1) * self.itemSpacing + CGFloat(rowCount) * itemSize + bottomInset
|
||||
}
|
||||
|
||||
func visibleRange(rect: CGRect) -> (Int, Int) {
|
||||
var minVisibleRow = Int(floor((rect.minY - self.itemSpacing) / (self.itemSize + self.itemSpacing)))
|
||||
minVisibleRow = max(0, minVisibleRow)
|
||||
var maxVisibleRow = Int(ceil((rect.maxY - self.itemSpacing) / (self.itemSize + itemSpacing)))
|
||||
maxVisibleRow = min(self.rowCount - 1, maxVisibleRow)
|
||||
|
||||
let minVisibleIndex = minVisibleRow * itemsInRow
|
||||
let maxVisibleIndex = min(self.itemCount - 1, (maxVisibleRow + 1) * itemsInRow - 1)
|
||||
|
||||
return (minVisibleIndex, maxVisibleIndex)
|
||||
}
|
||||
|
||||
func frame(forItemAt index: Int, sideInset: CGFloat) -> CGRect {
|
||||
let rowIndex = index / Int(self.itemsInRow)
|
||||
let columnIndex = index % Int(self.itemsInRow)
|
||||
let itemOrigin = CGPoint(x: sideInset + CGFloat(columnIndex) * (self.itemSize + self.itemSpacing), y: self.itemSpacing + CGFloat(rowIndex) * (self.itemSize + self.itemSpacing))
|
||||
return CGRect(origin: itemOrigin, size: CGSize(width: columnIndex == self.itemsInRow ? (self.containerWidth - itemOrigin.x) : self.itemSize, height: self.itemSize))
|
||||
}
|
||||
}
|
||||
|
||||
final class Balanced {
|
||||
let frames: [CGRect]
|
||||
let contentHeight: CGFloat
|
||||
|
||||
init(containerWidth: CGFloat, items: [VisualMediaItem]) {
|
||||
self.frames = calculateItemFrames(items: items, containerWidth: containerWidth)
|
||||
if let last = self.frames.last {
|
||||
self.contentHeight = last.maxY
|
||||
} else {
|
||||
self.contentHeight = 0.0
|
||||
}
|
||||
}
|
||||
|
||||
func visibleRange(rect: CGRect) -> (Int, Int) {
|
||||
for i in 0 ..< self.frames.count {
|
||||
if self.frames[i].maxY >= rect.minY {
|
||||
for j in i ..< self.frames.count {
|
||||
if self.frames[j].minY >= rect.maxY {
|
||||
return (i, j - 1)
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
return (i, self.frames.count - 1)
|
||||
}
|
||||
return (0, -1)
|
||||
}
|
||||
|
||||
func frame(forItemAt index: Int, sideInset: CGFloat) -> CGRect {
|
||||
if index >= 0 && index < self.frames.count {
|
||||
return self.frames[index]
|
||||
} else {
|
||||
assertionFailure()
|
||||
return CGRect(origin: CGPoint(), size: CGSize(width: 100.0, height: 100.0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case grid(Grid)
|
||||
case balanced(Balanced)
|
||||
|
||||
var contentHeight: CGFloat {
|
||||
switch self {
|
||||
case let .grid(grid):
|
||||
return grid.contentHeight
|
||||
case let .balanced(balanced):
|
||||
return balanced.contentHeight
|
||||
}
|
||||
}
|
||||
|
||||
func visibleRange(rect: CGRect) -> (Int, Int) {
|
||||
switch self {
|
||||
case let .grid(grid):
|
||||
return grid.visibleRange(rect: rect)
|
||||
case let .balanced(balanced):
|
||||
return balanced.visibleRange(rect: rect)
|
||||
}
|
||||
}
|
||||
|
||||
func frame(forItemAt index: Int, sideInset: CGFloat) -> CGRect {
|
||||
switch self {
|
||||
case let .grid(grid):
|
||||
return grid.frame(forItemAt: index, sideInset: sideInset)
|
||||
case let .balanced(balanced):
|
||||
return balanced.frame(forItemAt: index, sideInset: sideInset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate {
|
||||
enum ContentType {
|
||||
case photoOrVideo
|
||||
case gifs
|
||||
}
|
||||
|
||||
private let context: AccountContext
|
||||
private let peerId: PeerId
|
||||
private let chatControllerInteraction: ChatControllerInteraction
|
||||
private let contentType: ContentType
|
||||
|
||||
private let scrollNode: ASScrollNode
|
||||
private let floatingHeaderNode: FloatingHeaderNode
|
||||
@ -462,6 +643,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
||||
private let listDisposable = MetaDisposable()
|
||||
private var hiddenMediaDisposable: Disposable?
|
||||
private var mediaItems: [VisualMediaItem] = []
|
||||
private var itemsLayout: ItemsLayout?
|
||||
private var visibleMediaItems: [UInt32: VisualMediaItemNode] = [:]
|
||||
|
||||
private var numberOfItemsToRequest: Int = 50
|
||||
@ -471,10 +653,11 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
||||
|
||||
private var decelerationAnimator: ConstantDisplayLinkAnimator?
|
||||
|
||||
init(context: AccountContext, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId) {
|
||||
init(context: AccountContext, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId, contentType: ContentType) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
self.chatControllerInteraction = chatControllerInteraction
|
||||
self.contentType = contentType
|
||||
|
||||
self.scrollNode = ASScrollNode()
|
||||
self.floatingHeaderNode = FloatingHeaderNode()
|
||||
@ -536,7 +719,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
||||
return
|
||||
}
|
||||
self.isRequestingView = true
|
||||
self.listDisposable.set((self.context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(self.peerId), index: .upperBound, anchorIndex: .upperBound, count: self.numberOfItemsToRequest, fixedCombinedReadStates: nil, tagMask: .photoOrVideo)
|
||||
self.listDisposable.set((self.context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(self.peerId), index: .upperBound, anchorIndex: .upperBound, count: self.numberOfItemsToRequest, fixedCombinedReadStates: nil, tagMask: tagMaskForType(self.contentType))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] (view, updateType, _) in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
@ -557,6 +740,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
||||
for entry in view.entries.reversed() {
|
||||
self.mediaItems.append(VisualMediaItem(message: entry.message))
|
||||
}
|
||||
self.itemsLayout = nil
|
||||
|
||||
let wasFirstHistoryView = self.isFirstHistoryView
|
||||
self.isFirstHistoryView = false
|
||||
@ -675,15 +859,20 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
||||
|
||||
let availableWidth = size.width - sideInset * 2.0
|
||||
|
||||
let itemSpacing: CGFloat = 1.0
|
||||
let itemsInRow: Int = max(3, min(6, Int(availableWidth / 140.0)))
|
||||
let itemSize: CGFloat = floor(availableWidth / CGFloat(itemsInRow))
|
||||
let itemsLayout: ItemsLayout
|
||||
if let current = self.itemsLayout {
|
||||
itemsLayout = current
|
||||
} else {
|
||||
switch self.contentType {
|
||||
case .photoOrVideo:
|
||||
itemsLayout = .grid(ItemsLayout.Grid(containerWidth: availableWidth, itemCount: self.mediaItems.count, bottomInset: bottomInset))
|
||||
case .gifs:
|
||||
itemsLayout = .balanced(ItemsLayout.Balanced(containerWidth: availableWidth, items: self.mediaItems))
|
||||
}
|
||||
self.itemsLayout = itemsLayout
|
||||
}
|
||||
|
||||
let rowCount: Int = self.mediaItems.count / itemsInRow + (self.mediaItems.count % itemsInRow == 0 ? 0 : 1)
|
||||
|
||||
let contentHeight = CGFloat(rowCount + 1) * itemSpacing + CGFloat(rowCount) * itemSize + bottomInset
|
||||
|
||||
self.scrollNode.view.contentSize = CGSize(width: size.width, height: contentHeight)
|
||||
self.scrollNode.view.contentSize = CGSize(width: size.width, height: itemsLayout.contentHeight)
|
||||
self.updateVisibleItems(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, theme: presentationData.theme, strings: presentationData.strings, synchronousLoad: synchronous)
|
||||
|
||||
if isScrollingLockedAtTop {
|
||||
@ -736,23 +925,15 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
||||
}
|
||||
|
||||
private func updateVisibleItems(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, theme: PresentationTheme, strings: PresentationStrings, synchronousLoad: Bool) {
|
||||
let availableWidth = size.width - sideInset * 2.0
|
||||
|
||||
let itemSpacing: CGFloat = 1.0
|
||||
let itemsInRow: Int = max(3, min(6, Int(availableWidth / 140.0)))
|
||||
let itemSize: CGFloat = floor(availableWidth / CGFloat(itemsInRow))
|
||||
|
||||
let rowCount: Int = self.mediaItems.count / itemsInRow + (self.mediaItems.count % itemsInRow == 0 ? 0 : 1)
|
||||
guard let itemsLayout = self.itemsLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
let headerItemMinY = self.scrollNode.view.bounds.minY + 20.0
|
||||
let visibleRect = self.scrollNode.view.bounds.insetBy(dx: 0.0, dy: -400.0)
|
||||
var minVisibleRow = Int(floor((visibleRect.minY - itemSpacing) / (itemSize + itemSpacing)))
|
||||
minVisibleRow = max(0, minVisibleRow)
|
||||
var maxVisibleRow = Int(ceil((visibleRect.maxY - itemSpacing) / (itemSize + itemSpacing)))
|
||||
maxVisibleRow = min(rowCount - 1, maxVisibleRow)
|
||||
let activeRect = self.scrollNode.view.bounds
|
||||
let visibleRect = activeRect.insetBy(dx: 0.0, dy: -400.0)
|
||||
|
||||
let minVisibleIndex = minVisibleRow * itemsInRow
|
||||
let maxVisibleIndex = min(self.mediaItems.count - 1, (maxVisibleRow + 1) * itemsInRow - 1)
|
||||
let (minVisibleIndex, maxVisibleIndex) = itemsLayout.visibleRange(rect: visibleRect)
|
||||
|
||||
var headerItem: Message?
|
||||
|
||||
@ -761,10 +942,9 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
||||
for i in minVisibleIndex ... maxVisibleIndex {
|
||||
let stableId = self.mediaItems[i].message.stableId
|
||||
validIds.insert(stableId)
|
||||
let rowIndex = i / Int(itemsInRow)
|
||||
let columnIndex = i % Int(itemsInRow)
|
||||
let itemOrigin = CGPoint(x: sideInset + CGFloat(columnIndex) * (itemSize + itemSpacing), y: itemSpacing + CGFloat(rowIndex) * (itemSize + itemSpacing))
|
||||
let itemFrame = CGRect(origin: itemOrigin, size: CGSize(width: columnIndex == itemsInRow ? (availableWidth - itemOrigin.x) : itemSize, height: itemSize))
|
||||
|
||||
let itemFrame = itemsLayout.frame(forItemAt: i, sideInset: sideInset)
|
||||
|
||||
let itemNode: VisualMediaItemNode
|
||||
if let current = self.visibleMediaItems[stableId] {
|
||||
itemNode = current
|
||||
@ -782,6 +962,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
||||
itemSynchronousLoad = synchronousLoad
|
||||
}
|
||||
itemNode.update(size: itemFrame.size, item: self.mediaItems[i], theme: theme, synchronousLoad: itemSynchronousLoad)
|
||||
itemNode.updateIsVisible(itemFrame.intersects(activeRect))
|
||||
}
|
||||
}
|
||||
var removeKeys: [UInt32] = []
|
||||
@ -872,3 +1053,201 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private func NH_LP_TABLE_LOOKUP(_ table: inout [Int], _ i: Int, _ j: Int, _ rowsize: Int) -> Int {
|
||||
return table[i * rowsize + j]
|
||||
}
|
||||
|
||||
private func NH_LP_TABLE_LOOKUP_SET(_ table: inout [Int], _ i: Int, _ j: Int, _ rowsize: Int, _ value: Int) {
|
||||
table[i * rowsize + j] = value
|
||||
}
|
||||
|
||||
private func linearPartitionTable(_ weights: [Int], numberOfPartitions: Int) -> [Int] {
|
||||
let n = weights.count
|
||||
let k = numberOfPartitions
|
||||
|
||||
let tableSize = n * k;
|
||||
var tmpTable = Array<Int>(repeatElement(0, count: tableSize))
|
||||
|
||||
let solutionSize = (n - 1) * (k - 1)
|
||||
var solution = Array<Int>(repeatElement(0, count: solutionSize))
|
||||
|
||||
for i in 0 ..< n {
|
||||
let offset = i != 0 ? NH_LP_TABLE_LOOKUP(&tmpTable, i - 1, 0, k) : 0
|
||||
NH_LP_TABLE_LOOKUP_SET(&tmpTable, i, 0, k, Int(weights[i]) + offset)
|
||||
}
|
||||
|
||||
for j in 0 ..< k {
|
||||
NH_LP_TABLE_LOOKUP_SET(&tmpTable, 0, j, k, Int(weights[0]))
|
||||
}
|
||||
|
||||
for i in 1 ..< n {
|
||||
for j in 1 ..< k {
|
||||
var currentMin = 0
|
||||
var minX = Int.max
|
||||
|
||||
for x in 0 ..< i {
|
||||
let c1 = NH_LP_TABLE_LOOKUP(&tmpTable, x, j - 1, k)
|
||||
let c2 = NH_LP_TABLE_LOOKUP(&tmpTable, i, 0, k) - NH_LP_TABLE_LOOKUP(&tmpTable, x, 0, k)
|
||||
let cost = max(c1, c2)
|
||||
|
||||
if x == 0 || cost < currentMin {
|
||||
currentMin = cost;
|
||||
minX = x
|
||||
}
|
||||
}
|
||||
|
||||
NH_LP_TABLE_LOOKUP_SET(&tmpTable, i, j, k, currentMin)
|
||||
NH_LP_TABLE_LOOKUP_SET(&solution, i - 1, j - 1, k - 1, minX)
|
||||
}
|
||||
}
|
||||
|
||||
return solution
|
||||
}
|
||||
|
||||
private func linearPartitionForWeights(_ weights: [Int], numberOfPartitions: Int) -> [[Int]] {
|
||||
var n = weights.count
|
||||
var k = numberOfPartitions
|
||||
|
||||
if k <= 0 {
|
||||
return []
|
||||
}
|
||||
|
||||
if k >= n {
|
||||
var partition: [[Int]] = []
|
||||
for weight in weights {
|
||||
partition.append([weight])
|
||||
}
|
||||
return partition
|
||||
}
|
||||
|
||||
if n == 1 {
|
||||
return [weights]
|
||||
}
|
||||
|
||||
var solution = linearPartitionTable(weights, numberOfPartitions: numberOfPartitions)
|
||||
let solutionRowSize = numberOfPartitions - 1
|
||||
|
||||
k = k - 2;
|
||||
n = n - 1;
|
||||
|
||||
var answer: [[Int]] = []
|
||||
|
||||
while k >= 0 {
|
||||
if n < 1 {
|
||||
answer.insert([], at: 0)
|
||||
} else {
|
||||
var currentAnswer: [Int] = []
|
||||
|
||||
var i = NH_LP_TABLE_LOOKUP(&solution, n - 1, k, solutionRowSize) + 1
|
||||
let range = n + 1
|
||||
while i < range {
|
||||
currentAnswer.append(weights[i])
|
||||
i += 1
|
||||
}
|
||||
|
||||
answer.insert(currentAnswer, at: 0)
|
||||
|
||||
n = NH_LP_TABLE_LOOKUP(&solution, n - 1, k, solutionRowSize)
|
||||
}
|
||||
|
||||
k = k - 1
|
||||
}
|
||||
|
||||
var currentAnswer: [Int] = []
|
||||
var i = 0
|
||||
let range = n + 1
|
||||
while i < range {
|
||||
currentAnswer.append(weights[i])
|
||||
i += 1
|
||||
}
|
||||
|
||||
answer.insert(currentAnswer, at: 0)
|
||||
|
||||
return answer
|
||||
}
|
||||
|
||||
private func calculateItemFrames(items: [VisualMediaItem], containerWidth: CGFloat) -> [CGRect] {
|
||||
var frames: [CGRect] = []
|
||||
|
||||
var weights: [Int] = []
|
||||
for item in items {
|
||||
weights.append(Int(item.aspectRatio * 100))
|
||||
}
|
||||
|
||||
let preferredRowSize: CGFloat = 160.0
|
||||
let idealHeight: CGFloat = preferredRowSize
|
||||
|
||||
var totalItemSize: CGFloat = 0.0
|
||||
for i in 0 ..< items.count {
|
||||
totalItemSize += items[i].aspectRatio * idealHeight
|
||||
}
|
||||
let numberOfRows = max(Int(round(totalItemSize / containerWidth)), 1)
|
||||
|
||||
let partition = linearPartitionForWeights(weights, numberOfPartitions:numberOfRows)
|
||||
|
||||
var i = 0
|
||||
var offset = CGPoint(x: 0.0, y: 0.0)
|
||||
var previousItemSize: CGFloat = 0.0
|
||||
let maxWidth = containerWidth
|
||||
|
||||
let minimumInteritemSpacing: CGFloat = 1.0
|
||||
let minimumLineSpacing: CGFloat = 1.0
|
||||
|
||||
let viewportWidth: CGFloat = containerWidth
|
||||
|
||||
var rowIndex = -1
|
||||
for row in partition {
|
||||
rowIndex += 1
|
||||
|
||||
var summedRatios: CGFloat = 0.0
|
||||
|
||||
var j = i
|
||||
var n = i + row.count
|
||||
|
||||
while j < n {
|
||||
summedRatios += items[j].aspectRatio
|
||||
|
||||
j += 1
|
||||
}
|
||||
|
||||
var rowSize = containerWidth - (CGFloat(row.count - 1) * minimumInteritemSpacing)
|
||||
|
||||
if rowIndex == partition.count - 1 {
|
||||
if row.count < 2 {
|
||||
rowSize = floor(viewportWidth / 3.0) - (CGFloat(row.count - 1) * minimumInteritemSpacing)
|
||||
} else if row.count < 3 {
|
||||
rowSize = floor(viewportWidth * 2.0 / 3.0) - (CGFloat(row.count - 1) * minimumInteritemSpacing)
|
||||
}
|
||||
}
|
||||
|
||||
j = i
|
||||
n = i + row.count
|
||||
|
||||
while j < n {
|
||||
let preferredAspectRatio = items[j].aspectRatio
|
||||
|
||||
let actualSize = CGSize(width: round(rowSize / summedRatios * (preferredAspectRatio)), height: preferredRowSize)
|
||||
|
||||
var frame = CGRect(x: offset.x, y: offset.y, width: actualSize.width, height: actualSize.height)
|
||||
if frame.origin.x + frame.size.width >= maxWidth - 2.0 {
|
||||
frame.size.width = max(1.0, maxWidth - frame.origin.x)
|
||||
}
|
||||
|
||||
frames.append(frame)
|
||||
|
||||
offset.x += actualSize.width + minimumInteritemSpacing
|
||||
previousItemSize = actualSize.height
|
||||
|
||||
j += 1
|
||||
}
|
||||
|
||||
if row.count > 0 {
|
||||
offset = CGPoint(x: 0.0, y: offset.y + previousItemSize + minimumLineSpacing)
|
||||
}
|
||||
|
||||
i += row.count
|
||||
}
|
||||
|
||||
return frames
|
||||
}
|
||||
|
@ -114,7 +114,8 @@ private func peerInfoAvailableMediaPanes(context: AccountContext, peerId: PeerId
|
||||
(.file, .files),
|
||||
(.music, .music),
|
||||
(.voiceOrInstantVideo, .voice),
|
||||
(.webPage, .links)
|
||||
(.webPage, .links),
|
||||
(.gif, .gifs)
|
||||
]
|
||||
enum PaneState {
|
||||
case loading
|
||||
@ -174,8 +175,8 @@ enum PeerInfoMembersData: Equatable {
|
||||
|
||||
var membersContext: PeerInfoMembersContext {
|
||||
switch self {
|
||||
case let .shortList(shortList):
|
||||
return shortList.membersContext
|
||||
case let .shortList(membersContext, _):
|
||||
return membersContext
|
||||
case let .longList(membersContext):
|
||||
return membersContext
|
||||
}
|
||||
|
@ -53,6 +53,7 @@ enum PeerInfoPaneKey {
|
||||
case links
|
||||
case voice
|
||||
case music
|
||||
case gifs
|
||||
case groupsInCommon
|
||||
case members
|
||||
}
|
||||
@ -380,7 +381,7 @@ private final class PeerInfoPendingPane {
|
||||
let paneNode: PeerInfoPaneNode
|
||||
switch key {
|
||||
case .media:
|
||||
paneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId)
|
||||
paneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, contentType: .photoOrVideo)
|
||||
case .files:
|
||||
paneNode = PeerInfoListPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .file)
|
||||
case .links:
|
||||
@ -389,6 +390,8 @@ private final class PeerInfoPendingPane {
|
||||
paneNode = PeerInfoListPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .voiceOrInstantVideo)
|
||||
case .music:
|
||||
paneNode = PeerInfoListPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .music)
|
||||
case .gifs:
|
||||
paneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, contentType: .gifs)
|
||||
case .groupsInCommon:
|
||||
paneNode = PeerInfoGroupsInCommonPaneNode(context: context, peerId: peerId, chatControllerInteraction: chatControllerInteraction, openPeerContextAction: openPeerContextAction, groupsInCommonContext: data.groupsInCommon!)
|
||||
case .members:
|
||||
@ -838,6 +841,8 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
|
||||
title = presentationData.strings.PeerInfo_PaneLinks
|
||||
case .voice:
|
||||
title = presentationData.strings.PeerInfo_PaneVoiceAndVideo
|
||||
case .gifs:
|
||||
title = presentationData.strings.PeerInfo_PaneGifs
|
||||
case .music:
|
||||
title = presentationData.strings.PeerInfo_PaneAudio
|
||||
case .groupsInCommon:
|
||||
|
@ -85,7 +85,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
self.segmentedControlNode = nil
|
||||
}
|
||||
|
||||
self.chatListNode = ChatListNode(context: context, groupId: .root, previewing: false, fillPreloadItems: false, mode: .peers(filter: filter, isSelecting: false, additionalCategories: []), theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations)
|
||||
self.chatListNode = ChatListNode(context: context, groupId: .root, previewing: false, fillPreloadItems: false, mode: .peers(filter: filter, isSelecting: false, additionalCategories: [], chatListFilters: nil), theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations)
|
||||
|
||||
super.init()
|
||||
|
||||
|
@ -18,6 +18,7 @@ public enum UndoOverlayContent {
|
||||
case actionSucceeded(title: String, text: String, cancel: String)
|
||||
case stickersModified(title: String, text: String, undo: Bool, info: StickerPackCollectionInfo, topItem: ItemCollectionItem?, account: Account)
|
||||
case dice(dice: TelegramMediaDice, account: Account, text: String, action: String?)
|
||||
case chatAddedToFolder(chatTitle: String, folderTitle: String)
|
||||
}
|
||||
|
||||
public enum UndoOverlayAction {
|
||||
|
@ -168,6 +168,22 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
displayUndo = true
|
||||
undoText = cancel
|
||||
self.originalRemainingSeconds = 5
|
||||
case let .chatAddedToFolder(chatTitle, folderTitle):
|
||||
self.iconNode = nil
|
||||
self.iconCheckNode = nil
|
||||
self.animationNode = AnimationNode(animation: "anim_success", colors: ["info1.info1.stroke": self.animationBackgroundColor, "info2.info2.Fill": self.animationBackgroundColor], scale: 1.0)
|
||||
self.animatedStickerNode = nil
|
||||
|
||||
let (rawString, attributes) = presentationData.strings.ChatList_AddedToFolderTooltip(chatTitle, folderTitle)
|
||||
|
||||
let string = NSMutableAttributedString(attributedString: NSAttributedString(string: rawString, font: Font.regular(14.0), textColor: .white))
|
||||
for (_, range) in attributes {
|
||||
string.addAttribute(.font, value: Font.regular(14.0), range: range)
|
||||
}
|
||||
|
||||
self.textNode.attributedText = string
|
||||
displayUndo = false
|
||||
self.originalRemainingSeconds = 5
|
||||
case let .emoji(path, text):
|
||||
self.iconNode = nil
|
||||
self.iconCheckNode = nil
|
||||
@ -352,7 +368,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
||||
switch content {
|
||||
case .removedChat:
|
||||
self.panelWrapperNode.addSubnode(self.timerTextNode)
|
||||
case .archivedChat, .hidArchive, .revealedArchive, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified:
|
||||
case .archivedChat, .hidArchive, .revealedArchive, .succeed, .emoji, .swipeToReply, .actionSucceeded, .stickersModified, .chatAddedToFolder:
|
||||
break
|
||||
case .dice:
|
||||
self.panelWrapperNode.clipsToBounds = true
|
||||
|
Binary file not shown.
@ -449,12 +449,12 @@ public final class WalletStrings: Equatable {
|
||||
public var Wallet_SecureStorageReset_Title: String { return self._s[219]! }
|
||||
public var Wallet_Receive_CommentHeader: String { return self._s[220]! }
|
||||
public var Wallet_Info_ReceiveGrams: String { return self._s[221]! }
|
||||
public func Wallet_Updated_MinutesAgo(_ value: Int32) -> String {
|
||||
public func Wallet_Updated_HoursAgo(_ value: Int32) -> String {
|
||||
let form = getPluralizationForm(self.lc, value)
|
||||
let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator)
|
||||
return String(format: self._ps[0 * 6 + Int(form.rawValue)]!, stringValue)
|
||||
}
|
||||
public func Wallet_Updated_HoursAgo(_ value: Int32) -> String {
|
||||
public func Wallet_Updated_MinutesAgo(_ value: Int32) -> String {
|
||||
let form = getPluralizationForm(self.lc, value)
|
||||
let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator)
|
||||
return String(format: self._ps[1 * 6 + Int(form.rawValue)]!, stringValue)
|
||||
|
Loading…
x
Reference in New Issue
Block a user