Updates too many to describe

This commit is contained in:
Ali 2020-05-08 18:42:36 +04:00
parent ba6cc80b60
commit 04efb74bfa
42 changed files with 4558 additions and 3863 deletions

View File

@ -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$@";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -63,7 +63,7 @@ public func tagsForStoreMessage(incoming: Bool, attributes: [MessageAttribute],
}
}
if isAnimated {
refinedTag = nil
refinedTag = .gif
}
if file.isAnimatedSticker {
refinedTag = nil

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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