mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-07 09:20:08 +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.PaneAudio" = "Audio";
|
||||||
"PeerInfo.PaneGroups" = "Groups";
|
"PeerInfo.PaneGroups" = "Groups";
|
||||||
"PeerInfo.PaneMembers" = "Members";
|
"PeerInfo.PaneMembers" = "Members";
|
||||||
|
"PeerInfo.PaneGifs" = "GIFs";
|
||||||
|
|
||||||
"PeerInfo.AddToContacts" = "Add to Contacts";
|
"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";
|
"PeerInfo.GroupAboutItem" = "about";
|
||||||
|
|
||||||
"Widget.ApplicationStartRequired" = "Open the app to use the widget";
|
"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 Display
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import Postbox
|
import Postbox
|
||||||
|
import TelegramCore
|
||||||
|
|
||||||
public struct ChatListNodeAdditionalCategory {
|
public struct ChatListNodeAdditionalCategory {
|
||||||
public var id: Int
|
public var id: Int
|
||||||
@ -30,7 +31,7 @@ public enum ContactMultiselectionControllerMode {
|
|||||||
case groupCreation
|
case groupCreation
|
||||||
case peerSelection(searchChatList: Bool, searchGroups: Bool, searchChannels: Bool)
|
case peerSelection(searchChatList: Bool, searchGroups: Bool, searchChannels: Bool)
|
||||||
case channelCreation
|
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 {
|
public enum ContactListFilter {
|
||||||
|
|||||||
@ -11,9 +11,11 @@ import TelegramUIPreferences
|
|||||||
import OverlayStatusController
|
import OverlayStatusController
|
||||||
import AlertUI
|
import AlertUI
|
||||||
import PresentationDataUtils
|
import PresentationDataUtils
|
||||||
|
import UndoUI
|
||||||
|
|
||||||
func archiveContextMenuItems(context: AccountContext, groupId: PeerGroupId, chatListController: ChatListControllerImpl?) -> Signal<[ContextMenuItem], NoError> {
|
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
|
return context.account.postbox.transaction { [weak chatListController] transaction -> [ContextMenuItem] in
|
||||||
var items: [ContextMenuItem] = []
|
var items: [ContextMenuItem] = []
|
||||||
|
|
||||||
@ -45,7 +47,8 @@ enum ChatContextMenuSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: ChatListNodeEntryPromoInfo?, source: ChatContextMenuSource, chatListController: ChatListControllerImpl?) -> Signal<[ContextMenuItem], NoError> {
|
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
|
return context.account.postbox.transaction { [weak chatListController] transaction -> [ContextMenuItem] in
|
||||||
if promoInfo != nil {
|
if promoInfo != nil {
|
||||||
return []
|
return []
|
||||||
@ -107,6 +110,91 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
|
|||||||
}
|
}
|
||||||
|
|
||||||
if case .chatList = source {
|
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 {
|
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
|
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()
|
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
|
var found = false
|
||||||
for filter in presetList {
|
for filter in presetList {
|
||||||
if filter.id == id {
|
if filter.id == id {
|
||||||
strongSelf.push(chatListFilterAddChatsController(context: strongSelf.context, filter: filter))
|
let _ = (currentChatListFilters(postbox: strongSelf.context.account.postbox)
|
||||||
f(.dismissWithoutContent)
|
|> 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
|
found = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@ -545,11 +545,11 @@ private enum AdditionalExcludeCategoryId: Int {
|
|||||||
case archived
|
case archived
|
||||||
}
|
}
|
||||||
|
|
||||||
func chatListFilterAddChatsController(context: AccountContext, filter: ChatListFilter) -> ViewController {
|
func chatListFilterAddChatsController(context: AccountContext, filter: ChatListFilter, allFilters: [ChatListFilter]) -> ViewController {
|
||||||
return internalChatListFilterAddChatsController(context: context, filter: filter, applyAutomatically: true, updated: { _ in })
|
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 presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||||
let additionalCategories: [ChatListNodeAdditionalCategory] = [
|
let additionalCategories: [ChatListNodeAdditionalCategory] = [
|
||||||
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
|
controller.navigationPresentation = .modal
|
||||||
let _ = (controller.result
|
let _ = (controller.result
|
||||||
|> take(1)
|
|> take(1)
|
||||||
@ -649,7 +649,7 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
|
|||||||
return controller
|
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 presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||||
let additionalCategories: [ChatListNodeAdditionalCategory] = [
|
let additionalCategories: [ChatListNodeAdditionalCategory] = [
|
||||||
ChatListNodeAdditionalCategory(
|
ChatListNodeAdditionalCategory(
|
||||||
@ -679,7 +679,7 @@ private func internalChatListFilterExcludeChatsController(context: AccountContex
|
|||||||
selectedCategories.insert(AdditionalExcludeCategoryId.archived.rawValue)
|
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
|
controller.navigationPresentation = .modal
|
||||||
let _ = (controller.result
|
let _ = (controller.result
|
||||||
|> take(1)
|
|> take(1)
|
||||||
@ -834,17 +834,20 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
|
|||||||
includePeers.setPeers(state.additionallyIncludePeers)
|
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 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
|
let _ = (currentChatListFilters(postbox: context.account.postbox)
|
||||||
skipStateAnimation = true
|
|> deliverOnMainQueue).start(next: { filters in
|
||||||
updateState { state in
|
let controller = internalChatListFilterAddChatsController(context: context, filter: filter, allFilters: filters, applyAutomatically: false, updated: { filter in
|
||||||
var state = state
|
skipStateAnimation = true
|
||||||
state.additionallyIncludePeers = filter.data.includePeers.peers
|
updateState { state in
|
||||||
state.additionallyExcludePeers = filter.data.excludePeers
|
var state = state
|
||||||
state.includeCategories = filter.data.categories
|
state.additionallyIncludePeers = filter.data.includePeers.peers
|
||||||
return state
|
state.additionallyExcludePeers = filter.data.excludePeers
|
||||||
}
|
state.includeCategories = filter.data.categories
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
})
|
||||||
|
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||||
})
|
})
|
||||||
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
|
||||||
},
|
},
|
||||||
openAddExcludePeer: {
|
openAddExcludePeer: {
|
||||||
let state = stateValue.with { $0 }
|
let state = stateValue.with { $0 }
|
||||||
@ -852,20 +855,23 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
|
|||||||
includePeers.setPeers(state.additionallyIncludePeers)
|
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 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
|
let _ = (currentChatListFilters(postbox: context.account.postbox)
|
||||||
skipStateAnimation = true
|
|> deliverOnMainQueue).start(next: { filters in
|
||||||
updateState { state in
|
let controller = internalChatListFilterExcludeChatsController(context: context, filter: filter, allFilters: filters, applyAutomatically: false, updated: { filter in
|
||||||
var state = state
|
skipStateAnimation = true
|
||||||
state.additionallyIncludePeers = filter.data.includePeers.peers
|
updateState { state in
|
||||||
state.additionallyExcludePeers = filter.data.excludePeers
|
var state = state
|
||||||
state.includeCategories = filter.data.categories
|
state.additionallyIncludePeers = filter.data.includePeers.peers
|
||||||
state.excludeRead = filter.data.excludeRead
|
state.additionallyExcludePeers = filter.data.excludePeers
|
||||||
state.excludeMuted = filter.data.excludeMuted
|
state.includeCategories = filter.data.categories
|
||||||
state.excludeArchived = filter.data.excludeArchived
|
state.excludeRead = filter.data.excludeRead
|
||||||
return state
|
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
|
deleteIncludePeer: { peerId in
|
||||||
updateState { state in
|
updateState { state in
|
||||||
|
|||||||
@ -144,9 +144,9 @@ private enum ChatListRecentEntry: Comparable, Identifiable {
|
|||||||
if let user = primaryPeer as? TelegramUser {
|
if let user = primaryPeer as? TelegramUser {
|
||||||
let servicePeer = isServicePeer(primaryPeer)
|
let servicePeer = isServicePeer(primaryPeer)
|
||||||
if user.flags.contains(.isSupport) && !servicePeer {
|
if user.flags.contains(.isSupport) && !servicePeer {
|
||||||
status = .custom(strings.Bot_GenericSupportStatus)
|
status = .custom(string: strings.Bot_GenericSupportStatus, multiline: false)
|
||||||
} else if let _ = user.botInfo {
|
} 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 {
|
} else if user.id != context.account.peerId && !servicePeer {
|
||||||
let presence = peer.presence ?? TelegramUserPresence(status: .none, lastActivity: 0)
|
let presence = peer.presence ?? TelegramUserPresence(status: .none, lastActivity: 0)
|
||||||
status = .presence(presence, timeFormat)
|
status = .presence(presence, timeFormat)
|
||||||
@ -154,19 +154,19 @@ private enum ChatListRecentEntry: Comparable, Identifiable {
|
|||||||
status = .none
|
status = .none
|
||||||
}
|
}
|
||||||
} else if let group = primaryPeer as? TelegramGroup {
|
} 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 {
|
} else if let channel = primaryPeer as? TelegramChannel {
|
||||||
if case .group = channel.info {
|
if case .group = channel.info {
|
||||||
if let count = peer.subpeerSummary?.count {
|
if let count = peer.subpeerSummary?.count {
|
||||||
status = .custom(strings.GroupInfo_ParticipantCount(Int32(count)))
|
status = .custom(string: strings.GroupInfo_ParticipantCount(Int32(count)), multiline: false)
|
||||||
} else {
|
} else {
|
||||||
status = .custom(strings.Group_Status)
|
status = .custom(string: strings.Group_Status, multiline: false)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if let count = peer.subpeerSummary?.count {
|
if let count = peer.subpeerSummary?.count {
|
||||||
status = .custom(strings.Conversation_StatusSubscribers(Int32(count)))
|
status = .custom(string: strings.Conversation_StatusSubscribers(Int32(count)), multiline: false)
|
||||||
} else {
|
} else {
|
||||||
status = .custom(strings.Channel_Status)
|
status = .custom(string: strings.Channel_Status, multiline: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import ChatListSearchItemHeader
|
|||||||
|
|
||||||
public enum ChatListNodeMode {
|
public enum ChatListNodeMode {
|
||||||
case chatList
|
case chatList
|
||||||
case peers(filter: ChatListNodePeersFilter, isSelecting: Bool, additionalCategories: [ChatListNodeAdditionalCategory])
|
case peers(filter: ChatListNodePeersFilter, isSelecting: Bool, additionalCategories: [ChatListNodeAdditionalCategory], chatListFilters: [ChatListFilter]?)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ChatListNodeListViewTransition {
|
struct ChatListNodeListViewTransition {
|
||||||
@ -180,7 +180,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
|
|||||||
switch mode {
|
switch mode {
|
||||||
case .chatList:
|
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)
|
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
|
let itemPeer = peer.chatMainPeer
|
||||||
var chatPeer: Peer?
|
var chatPeer: Peer?
|
||||||
if let peer = peer.peers[peer.peerId] {
|
if let peer = peer.peers[peer.peerId] {
|
||||||
@ -247,7 +247,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
|
|||||||
|
|
||||||
var header: ChatListSearchItemHeader?
|
var header: ChatListSearchItemHeader?
|
||||||
switch mode {
|
switch mode {
|
||||||
case let .peers(_, _, additionalCategories):
|
case let .peers(_, _, additionalCategories, _):
|
||||||
if !additionalCategories.isEmpty {
|
if !additionalCategories.isEmpty {
|
||||||
header = ChatListSearchItemHeader(type: .chats, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
|
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
|
var status: ContactsPeerItemStatus = .none
|
||||||
if isSelecting, let itemPeer = itemPeer {
|
if isSelecting, let itemPeer = itemPeer {
|
||||||
if let string = statusStringForPeerType(accountPeerId: context.account.peerId, strings: presentationData.strings, peer: itemPeer, isContact: isContact) {
|
let tagSummaryCount = summaryInfo.tagSummaryCount ?? 0
|
||||||
status = .custom(string)
|
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 {
|
} else {
|
||||||
status = .none
|
status = .none
|
||||||
}
|
}
|
||||||
@ -291,7 +294,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
|
|||||||
switch mode {
|
switch mode {
|
||||||
case .chatList:
|
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)
|
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
|
let itemPeer = peer.chatMainPeer
|
||||||
var chatPeer: Peer?
|
var chatPeer: Peer?
|
||||||
if let peer = peer.peers[peer.peerId] {
|
if let peer = peer.peers[peer.peerId] {
|
||||||
@ -314,7 +317,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
|
|||||||
}
|
}
|
||||||
var header: ChatListSearchItemHeader?
|
var header: ChatListSearchItemHeader?
|
||||||
switch mode {
|
switch mode {
|
||||||
case let .peers(_, _, additionalCategories):
|
case let .peers(_, _, additionalCategories, _):
|
||||||
if !additionalCategories.isEmpty {
|
if !additionalCategories.isEmpty {
|
||||||
header = ChatListSearchItemHeader(type: .chats, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil)
|
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
|
var status: ContactsPeerItemStatus = .none
|
||||||
if isSelecting, let itemPeer = itemPeer {
|
if isSelecting, let itemPeer = itemPeer {
|
||||||
if let string = statusStringForPeerType(accountPeerId: context.account.peerId, strings: presentationData.strings, peer: itemPeer, isContact: isContact) {
|
let tagSummaryCount = summaryInfo.tagSummaryCount ?? 0
|
||||||
status = .custom(string)
|
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 {
|
} else {
|
||||||
status = .none
|
status = .none
|
||||||
}
|
}
|
||||||
@ -514,7 +520,7 @@ public final class ChatListNode: ListView {
|
|||||||
self.mode = mode
|
self.mode = mode
|
||||||
|
|
||||||
var isSelecting = false
|
var isSelecting = false
|
||||||
if case .peers(_, true, _) = mode {
|
if case .peers(_, true, _, _) = mode {
|
||||||
isSelecting = true
|
isSelecting = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -674,7 +680,7 @@ public final class ChatListNode: ListView {
|
|||||||
let currentRemovingPeerId = self.currentRemovingPeerId
|
let currentRemovingPeerId = self.currentRemovingPeerId
|
||||||
|
|
||||||
let savedMessagesPeer: Signal<Peer?, NoError>
|
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)
|
savedMessagesPeer = context.account.postbox.loadedPeerWithId(context.account.peerId)
|
||||||
|> map(Optional.init)
|
|> map(Optional.init)
|
||||||
} else {
|
} else {
|
||||||
@ -727,7 +733,7 @@ public final class ChatListNode: ListView {
|
|||||||
switch mode {
|
switch mode {
|
||||||
case .chatList:
|
case .chatList:
|
||||||
return true
|
return true
|
||||||
case let .peers(filter, _, _):
|
case let .peers(filter, _, _, _):
|
||||||
guard !filter.contains(.excludeSavedMessages) || peer.peerId != currentPeerId else { return false }
|
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(.excludeSecretChats) || peer.peerId.namespace != Namespaces.Peer.SecretChat else { return false }
|
||||||
guard !filter.contains(.onlyPrivateChats) || peer.peerId.namespace == Namespaces.Peer.CloudUser 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 {
|
if accountPeerId == peer.id {
|
||||||
return nil
|
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 let user = peer as? TelegramUser {
|
||||||
if user.botInfo != nil || user.flags.contains(.isSupport) {
|
if user.botInfo != nil || user.flags.contains(.isSupport) {
|
||||||
return strings.ChatList_PeerTypeBot
|
return (strings.ChatList_PeerTypeBot, false)
|
||||||
} else if isContact {
|
} else if isContact {
|
||||||
return strings.ChatList_PeerTypeContact
|
return (strings.ChatList_PeerTypeContact, false)
|
||||||
} else {
|
} else {
|
||||||
return strings.ChatList_PeerTypeNonContact
|
return (strings.ChatList_PeerTypeNonContact, false)
|
||||||
}
|
}
|
||||||
} else if peer is TelegramSecretChat {
|
} else if peer is TelegramSecretChat {
|
||||||
if isContact {
|
if isContact {
|
||||||
return strings.ChatList_PeerTypeContact
|
return (strings.ChatList_PeerTypeContact, false)
|
||||||
} else {
|
} else {
|
||||||
return strings.ChatList_PeerTypeNonContact
|
return (strings.ChatList_PeerTypeNonContact, false)
|
||||||
}
|
}
|
||||||
} else if peer is TelegramGroup {
|
} else if peer is TelegramGroup {
|
||||||
return strings.ChatList_PeerTypeGroup
|
return (strings.ChatList_PeerTypeGroup, false)
|
||||||
} else if let channel = peer as? TelegramChannel {
|
} else if let channel = peer as? TelegramChannel {
|
||||||
if case .group = channel.info {
|
if case .group = channel.info {
|
||||||
return strings.ChatList_PeerTypeGroup
|
return (strings.ChatList_PeerTypeGroup, false)
|
||||||
} else {
|
} 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)
|
result.append(.HeaderEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
if view.laterIndex == nil, case let .peers(_, _, additionalCategories) = mode {
|
if view.laterIndex == nil, case let .peers(_, _, additionalCategories,
|
||||||
|
_) = mode {
|
||||||
var index = 0
|
var index = 0
|
||||||
for category in additionalCategories.reversed(){
|
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))
|
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)
|
let presence = presence ?? TelegramUserPresence(status: .none, lastActivity: 0)
|
||||||
status = .presence(presence, dateTimeFormat)
|
status = .presence(presence, dateTimeFormat)
|
||||||
} else if let group = peer as? TelegramGroup {
|
} 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 {
|
} else if let channel = peer as? TelegramChannel {
|
||||||
if case .group = channel.info {
|
if case .group = channel.info {
|
||||||
if let participantCount = participantCount, participantCount != 0 {
|
if let participantCount = participantCount, participantCount != 0 {
|
||||||
status = .custom(strings.Conversation_StatusMembers(participantCount))
|
status = .custom(string: strings.Conversation_StatusMembers(participantCount), multiline: false)
|
||||||
} else {
|
} else {
|
||||||
status = .custom(strings.Group_Status)
|
status = .custom(string: strings.Group_Status, multiline: false)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if let participantCount = participantCount, participantCount != 0 {
|
if let participantCount = participantCount, participantCount != 0 {
|
||||||
status = .custom(strings.Conversation_StatusSubscribers(participantCount))
|
status = .custom(string: strings.Conversation_StatusSubscribers(participantCount), multiline: false)
|
||||||
} else {
|
} else {
|
||||||
status = .custom(strings.Channel_Status)
|
status = .custom(string: strings.Channel_Status, multiline: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -54,7 +54,7 @@ private enum InviteContactsEntry: Comparable, Identifiable {
|
|||||||
case let .peer(_, id, contact, count, selection, theme, strings, nameSortOrder, nameDisplayOrder):
|
case let .peer(_, id, contact, count, selection, theme, strings, nameSortOrder, nameDisplayOrder):
|
||||||
let status: ContactsPeerItemStatus
|
let status: ContactsPeerItemStatus
|
||||||
if count != 0 {
|
if count != 0 {
|
||||||
status = .custom(strings.Contacts_ImportersCount(count))
|
status = .custom(string: strings.Contacts_ImportersCount(count), multiline: false)
|
||||||
} else {
|
} else {
|
||||||
status = .none
|
status = .none
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,7 +32,7 @@ public enum ContactsPeerItemStatus {
|
|||||||
case none
|
case none
|
||||||
case presence(PeerPresence, PresentationDateTimeFormat)
|
case presence(PeerPresence, PresentationDateTimeFormat)
|
||||||
case addressName(String)
|
case addressName(String)
|
||||||
case custom(String)
|
case custom(string: String, multiline: Bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ContactsPeerItemSelection: Equatable {
|
public enum ContactsPeerItemSelection: Equatable {
|
||||||
@ -499,6 +499,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
|||||||
|
|
||||||
var titleAttributedString: NSAttributedString?
|
var titleAttributedString: NSAttributedString?
|
||||||
var statusAttributedString: NSAttributedString?
|
var statusAttributedString: NSAttributedString?
|
||||||
|
var multilineStatus: Bool = false
|
||||||
var userPresence: TelegramUserPresence?
|
var userPresence: TelegramUserPresence?
|
||||||
|
|
||||||
switch item.peer {
|
switch item.peer {
|
||||||
@ -563,8 +564,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
|||||||
} else if !suffix.isEmpty {
|
} else if !suffix.isEmpty {
|
||||||
statusAttributedString = NSAttributedString(string: suffix, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
|
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)
|
statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
|
||||||
|
multilineStatus = multiline
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case let .deviceContact(_, contact):
|
case let .deviceContact(_, contact):
|
||||||
@ -585,8 +587,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch item.status {
|
switch item.status {
|
||||||
case let .custom(text):
|
case let .custom(text, multiline):
|
||||||
statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
|
statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
|
||||||
|
multilineStatus = multiline
|
||||||
default:
|
default:
|
||||||
break
|
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 (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 titleVerticalInset: CGFloat = statusAttributedString == nil ? 13.0 : 6.0
|
||||||
let verticalInset: 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 {
|
final class ContextActionsContainerNode: ASDisplayNode {
|
||||||
private let actionsNode: InnerActionsContainerNode
|
private let actionsNode: InnerActionsContainerNode
|
||||||
private let textSelectionTipNode: InnerTextSelectionTipContainerNode?
|
private let textSelectionTipNode: InnerTextSelectionTipContainerNode?
|
||||||
|
private let scrollNode: ASScrollNode
|
||||||
|
|
||||||
var panSelectionGestureEnabled: Bool = true {
|
var panSelectionGestureEnabled: Bool = true {
|
||||||
didSet {
|
didSet {
|
||||||
@ -411,10 +412,19 @@ final class ContextActionsContainerNode: ASDisplayNode {
|
|||||||
self.textSelectionTipNode = nil
|
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()
|
super.init()
|
||||||
|
|
||||||
self.addSubnode(self.actionsNode)
|
self.scrollNode.addSubnode(self.actionsNode)
|
||||||
self.textSelectionTipNode.flatMap(self.addSubnode)
|
self.textSelectionTipNode.flatMap(self.scrollNode.addSubnode)
|
||||||
|
self.addSubnode(self.scrollNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
|
func updateLayout(widthClass: ContainerViewLayoutSizeClass, constrainedWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||||
@ -433,6 +443,11 @@ final class ContextActionsContainerNode: ASDisplayNode {
|
|||||||
return contentSize
|
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? {
|
func actionNode(at point: CGPoint) -> ContextActionNode? {
|
||||||
return self.actionsNode.actionNode(at: self.view.convert(point, to: self.actionsNode.view))
|
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 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)
|
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
|
let contentSize = originalProjectedContentViewFrame.1.size
|
||||||
self.contentContainerNode.updateLayout(size: contentSize, scaledSize: contentSize, transition: transition)
|
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
|
let contentScale = (constrainedWidth - actionsSideInset * 2.0) / constrainedWidth
|
||||||
var contentUnscaledSize: CGSize
|
var contentUnscaledSize: CGSize
|
||||||
if case .compact = layout.metrics.widthClass {
|
if case .compact = layout.metrics.widthClass {
|
||||||
|
self.actionsContainerNode.updateSize(containerSize: actionsSize, contentSize: actionsSize)
|
||||||
|
|
||||||
let proposedContentHeight: CGFloat
|
let proposedContentHeight: CGFloat
|
||||||
if layout.size.width < layout.size.height {
|
if layout.size.width < layout.size.height {
|
||||||
proposedContentHeight = layout.size.height - topEdge - contentActionsSpacing - actionsSize.height - layout.intrinsicInsets.bottom - actionsBottomInset
|
proposedContentHeight = layout.size.height - topEdge - contentActionsSpacing - actionsSize.height - layout.intrinsicInsets.bottom - actionsBottomInset
|
||||||
} else {
|
} else {
|
||||||
proposedContentHeight = layout.size.height - topEdge - topEdge
|
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))
|
contentUnscaledSize = CGSize(width: constrainedWidth, height: max(100.0, proposedContentHeight))
|
||||||
|
|
||||||
@ -1249,6 +1255,9 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||||||
contentUnscaledSize = preferredSize
|
contentUnscaledSize = preferredSize
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
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))
|
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 let update: () -> Void
|
||||||
private var completed = false
|
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 {
|
public var isPaused: Bool = true {
|
||||||
didSet {
|
didSet {
|
||||||
if self.isPaused != oldValue {
|
if self.isPaused != oldValue {
|
||||||
|
|||||||
@ -22,7 +22,9 @@ private func tagsForMessage(_ message: Message) -> MessageTags? {
|
|||||||
return .photoOrVideo
|
return .photoOrVideo
|
||||||
case let file as TelegramMediaFile:
|
case let file as TelegramMediaFile:
|
||||||
if file.isVideo {
|
if file.isVideo {
|
||||||
if !file.isAnimated {
|
if file.isAnimated {
|
||||||
|
return .gif
|
||||||
|
} else {
|
||||||
return .photoOrVideo
|
return .photoOrVideo
|
||||||
}
|
}
|
||||||
} else if file.isVoice {
|
} else if file.isVoice {
|
||||||
|
|||||||
@ -148,7 +148,7 @@ private final class ChannelMembersSearchEntry: Comparable, Identifiable {
|
|||||||
case let .participant(participant, label, revealActions, revealed, enabled):
|
case let .participant(participant, label, revealActions, revealed, enabled):
|
||||||
let status: ContactsPeerItemStatus
|
let status: ContactsPeerItemStatus
|
||||||
if let label = label {
|
if let label = label {
|
||||||
status = .custom(label)
|
status = .custom(string: label, multiline: false)
|
||||||
} else if let presence = participant.presences[participant.peer.id], self.addIcon {
|
} else if let presence = participant.presences[participant.peer.id], self.addIcon {
|
||||||
status = .presence(presence, dateTimeFormat)
|
status = .presence(presence, dateTimeFormat)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -65,7 +65,7 @@ private enum ChannelMembersSearchEntry: Comparable, Identifiable {
|
|||||||
case let .peer(_, participant, editing, label, enabled):
|
case let .peer(_, participant, editing, label, enabled):
|
||||||
let status: ContactsPeerItemStatus
|
let status: ContactsPeerItemStatus
|
||||||
if let label = label {
|
if let label = label {
|
||||||
status = .custom(label)
|
status = .custom(string: label, multiline: false)
|
||||||
} else {
|
} else {
|
||||||
status = .none
|
status = .none
|
||||||
}
|
}
|
||||||
|
|||||||
@ -173,7 +173,7 @@ private enum OldChannelsEntry: ItemListNodeEntry {
|
|||||||
case let .peersHeader(title):
|
case let .peersHeader(title):
|
||||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
|
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
|
||||||
case let .peer(_, peer, selected):
|
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)
|
arguments.togglePeer(peer.peer.id, true)
|
||||||
}, setPeerIdWithRevealedOptions: nil, deletePeer: nil, itemHighlighting: nil, contextAction: nil)
|
}, 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 {
|
func item(context: AccountContext, presentationData: ItemListPresentationData, interaction: OldChannelsSearchInteraction) -> ListViewItem {
|
||||||
switch self {
|
switch self {
|
||||||
case let .peer(_, peer, selected):
|
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)
|
interaction.togglePeer(peer.peer.id)
|
||||||
}, setPeerIdWithRevealedOptions: nil, deletePeer: nil, itemHighlighting: nil, contextAction: nil)
|
}, setPeerIdWithRevealedOptions: nil, deletePeer: nil, itemHighlighting: nil, contextAction: nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -267,7 +267,7 @@ public struct ChatListFilterPredicate {
|
|||||||
self.include = include
|
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) {
|
if self.pinnedPeerIds.contains(peer.id) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@ -93,8 +93,9 @@ public extension MessageTags {
|
|||||||
static let voiceOrInstantVideo = MessageTags(rawValue: 1 << 4)
|
static let voiceOrInstantVideo = MessageTags(rawValue: 1 << 4)
|
||||||
static let unseenPersonalMessage = MessageTags(rawValue: 1 << 5)
|
static let unseenPersonalMessage = MessageTags(rawValue: 1 << 5)
|
||||||
static let liveLocation = MessageTags(rawValue: 1 << 6)
|
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 {
|
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]) {
|
public mutating func setPeers(_ peers: [PeerId]) {
|
||||||
self.peers = peers
|
self.peers = peers
|
||||||
self.pinnedPeers = self.pinnedPeers.filter { peers.contains($0) }
|
self.pinnedPeers = self.pinnedPeers.filter { peers.contains($0) }
|
||||||
@ -176,6 +191,18 @@ public struct ChatListFilterData: Equatable, Hashable {
|
|||||||
self.includePeers = includePeers
|
self.includePeers = includePeers
|
||||||
self.excludePeers = excludePeers
|
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 {
|
public struct ChatListFilter: PostboxCoding, Equatable {
|
||||||
|
|||||||
@ -462,7 +462,17 @@ func revalidateMediaResourceReference(postbox: Postbox, network: Network, revali
|
|||||||
if let partialReference = file.partialReference {
|
if let partialReference = file.partialReference {
|
||||||
updatedReference = partialReference.mediaReference(media.media).resourceReference(resource)
|
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?
|
var stickerPackReference: StickerPackReference?
|
||||||
for attribute in file.attributes {
|
for attribute in file.attributes {
|
||||||
if case let .Sticker(sticker) = attribute {
|
if case let .Sticker(sticker) = attribute {
|
||||||
@ -503,7 +513,13 @@ func revalidateMediaResourceReference(postbox: Postbox, network: Network, revali
|
|||||||
if let item = item as? StickerPackItem {
|
if let item = item as? StickerPackItem {
|
||||||
if media.id != nil && item.file.id == media.id {
|
if media.id != nil && item.file.id == media.id {
|
||||||
if let updatedResource = findUpdatedMediaResource(media: item.file, previousMedia: media, resource: resource) {
|
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
|
return Api.MessagesFilter.inputMessagesFilterUrl
|
||||||
} else if tagMask == .voiceOrInstantVideo {
|
} else if tagMask == .voiceOrInstantVideo {
|
||||||
return Api.MessagesFilter.inputMessagesFilterRoundVoice
|
return Api.MessagesFilter.inputMessagesFilterRoundVoice
|
||||||
|
} else if tagMask == .gif {
|
||||||
|
return Api.MessagesFilter.inputMessagesFilterGif
|
||||||
} else {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -271,7 +271,17 @@ private enum MultipartFetchSource {
|
|||||||
case master(location: MultipartFetchMasterLocation, download: DownloadWrapper)
|
case master(location: MultipartFetchMasterLocation, download: DownloadWrapper)
|
||||||
case cdn(masterDatacenterId: Int32, fileToken: Data, key: Data, iv: Data, download: DownloadWrapper, masterDownload: DownloadWrapper, hashSource: MultipartCdnHashSource)
|
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 {
|
switch self {
|
||||||
case .none:
|
case .none:
|
||||||
return .never()
|
return .never()
|
||||||
@ -281,7 +291,7 @@ private enum MultipartFetchSource {
|
|||||||
|
|
||||||
switch location {
|
switch location {
|
||||||
case let .generic(_, location):
|
case let .generic(_, location):
|
||||||
switch location(resource, resourceReference, fileReference) {
|
switch location(resource, resourceReferenceValue, fileReference) {
|
||||||
case .none:
|
case .none:
|
||||||
return .fail(.revalidateMediaReference)
|
return .fail(.revalidateMediaReference)
|
||||||
case .revalidate:
|
case .revalidate:
|
||||||
@ -382,13 +392,19 @@ private enum MultipartFetchSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum FetchResourceReference {
|
||||||
|
case empty
|
||||||
|
case forceRevalidate
|
||||||
|
case reference(MediaResourceReference)
|
||||||
|
}
|
||||||
|
|
||||||
private final class MultipartFetchManager {
|
private final class MultipartFetchManager {
|
||||||
let parallelParts: Int
|
let parallelParts: Int
|
||||||
let defaultPartSize = 128 * 1024
|
let defaultPartSize = 128 * 1024
|
||||||
var partAlignment = 4 * 1024
|
var partAlignment = 4 * 1024
|
||||||
|
|
||||||
var resource: TelegramMediaResource
|
var resource: TelegramMediaResource
|
||||||
var resourceReference: MediaResourceReference?
|
var resourceReference: FetchResourceReference
|
||||||
var fileReference: Data?
|
var fileReference: Data?
|
||||||
let parameters: MediaResourceFetchParameters?
|
let parameters: MediaResourceFetchParameters?
|
||||||
let consumerId: Int64
|
let consumerId: Int64
|
||||||
@ -441,10 +457,30 @@ private final class MultipartFetchManager {
|
|||||||
if let info = parameters?.info as? TelegramCloudMediaResourceFetchInfo {
|
if let info = parameters?.info as? TelegramCloudMediaResourceFetchInfo {
|
||||||
self.fileReference = info.reference.apiFileReference
|
self.fileReference = info.reference.apiFileReference
|
||||||
self.continueInBackground = info.continueInBackground
|
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 {
|
} else {
|
||||||
self.continueInBackground = false
|
self.continueInBackground = false
|
||||||
self.resourceReference = nil
|
self.resourceReference = .empty
|
||||||
}
|
}
|
||||||
|
|
||||||
self.state = MultipartDownloadState(encryptionKey: encryptionKey, decryptedSize: decryptedSize)
|
self.state = MultipartDownloadState(encryptionKey: encryptionKey, decryptedSize: decryptedSize)
|
||||||
@ -611,7 +647,6 @@ private final class MultipartFetchManager {
|
|||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var data = data
|
|
||||||
if data.count < downloadRange.count {
|
if data.count < downloadRange.count {
|
||||||
strongSelf.completeSize = downloadRange.lowerBound + data.count
|
strongSelf.completeSize = downloadRange.lowerBound + data.count
|
||||||
}
|
}
|
||||||
@ -639,7 +674,11 @@ private final class MultipartFetchManager {
|
|||||||
strongSelf.fileReference = reference
|
strongSelf.fileReference = reference
|
||||||
}
|
}
|
||||||
strongSelf.resource = validationResult.updatedResource
|
strongSelf.resource = validationResult.updatedResource
|
||||||
strongSelf.resourceReference = validationResult.updatedReference
|
if let reference = validationResult.updatedReference {
|
||||||
|
strongSelf.resourceReference = .reference(reference)
|
||||||
|
} else {
|
||||||
|
strongSelf.resourceReference = .empty
|
||||||
|
}
|
||||||
strongSelf.checkState()
|
strongSelf.checkState()
|
||||||
}
|
}
|
||||||
}, error: { _ in
|
}, error: { _ in
|
||||||
|
|||||||
@ -63,7 +63,7 @@ public func tagsForStoreMessage(incoming: Bool, attributes: [MessageAttribute],
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if isAnimated {
|
if isAnimated {
|
||||||
refinedTag = nil
|
refinedTag = .gif
|
||||||
}
|
}
|
||||||
if file.isAnimatedSticker {
|
if file.isAnimatedSticker {
|
||||||
refinedTag = nil
|
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 {
|
switch self.mode {
|
||||||
case let .chatSelection(_, selectedChats, additionalCategories):
|
case let .chatSelection(_, selectedChats, additionalCategories, _):
|
||||||
let _ = (self.context.account.postbox.transaction { transaction -> [Peer] in
|
let _ = (self.context.account.postbox.transaction { transaction -> [Peer] in
|
||||||
return selectedChats.compactMap(transaction.getPeer)
|
return selectedChats.compactMap(transaction.getPeer)
|
||||||
}
|
}
|
||||||
@ -425,7 +425,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
|
|||||||
break
|
break
|
||||||
case let .chats(chatsNode):
|
case let .chats(chatsNode):
|
||||||
var categoryToken: EditableTokenListToken?
|
var categoryToken: EditableTokenListToken?
|
||||||
if case let .chatSelection(_, _, additionalCategories) = strongSelf.mode {
|
if case let .chatSelection(_, _, additionalCategories, _) = strongSelf.mode {
|
||||||
if let additionalCategories = additionalCategories {
|
if let additionalCategories = additionalCategories {
|
||||||
for i in 0 ..< additionalCategories.categories.count {
|
for i in 0 ..< additionalCategories.categories.count {
|
||||||
if additionalCategories.categories[i].id == id {
|
if additionalCategories.categories[i].id == id {
|
||||||
|
|||||||
@ -84,9 +84,9 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
|
|||||||
placeholder = self.presentationData.strings.Compose_TokenListPlaceholder
|
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
|
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
|
chatListNode.updateState { state in
|
||||||
var state = state
|
var state = state
|
||||||
for peerId in selectedChats {
|
for peerId in selectedChats {
|
||||||
|
|||||||
@ -38,6 +38,11 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
|||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
private let interaction: VisualMediaItemInteraction
|
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 containerNode: ContextControllerSourceNode
|
||||||
private let imageNode: TransformImageNode
|
private let imageNode: TransformImageNode
|
||||||
private var statusNode: RadialStatusNode
|
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!)) {
|
if let media = media, (self.item?.1 == nil || !media.isEqual(to: self.item!.1!)) {
|
||||||
var mediaDimensions: CGSize?
|
var mediaDimensions: CGSize?
|
||||||
if let image = media as? TelegramMediaImage, let largestSize = largestImageRepresentation(image.representations)?.dimensions {
|
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
|
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.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
|
self.resourceStatus = nil
|
||||||
|
|
||||||
@ -290,6 +310,9 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
|||||||
|
|
||||||
self.containerNode.frame = imageFrame
|
self.containerNode.frame = imageFrame
|
||||||
self.imageNode.frame = imageFrame
|
self.imageNode.frame = imageFrame
|
||||||
|
if let sampleBufferLayer = self.sampleBufferLayer {
|
||||||
|
sampleBufferLayer.layer.frame = imageFrame
|
||||||
|
}
|
||||||
|
|
||||||
if let mediaDimensions = mediaDimensions {
|
if let mediaDimensions = mediaDimensions {
|
||||||
let imageSize = mediaDimensions.aspectFilled(imageFrame.size)
|
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) {
|
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
|
self.containerNode.isGestureEnabled = self.interaction.selectedMessageIds == nil
|
||||||
|
|
||||||
if let selectedIds = self.interaction.selectedMessageIds {
|
if let selectedIds = self.interaction.selectedMessageIds {
|
||||||
@ -379,9 +422,20 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
|||||||
|
|
||||||
private final class VisualMediaItem {
|
private final class VisualMediaItem {
|
||||||
let message: Message
|
let message: Message
|
||||||
|
let aspectRatio: CGFloat
|
||||||
|
|
||||||
init(message: Message) {
|
init(message: Message) {
|
||||||
self.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 {
|
final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScrollViewDelegate {
|
||||||
|
enum ContentType {
|
||||||
|
case photoOrVideo
|
||||||
|
case gifs
|
||||||
|
}
|
||||||
|
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
private let peerId: PeerId
|
private let peerId: PeerId
|
||||||
private let chatControllerInteraction: ChatControllerInteraction
|
private let chatControllerInteraction: ChatControllerInteraction
|
||||||
|
private let contentType: ContentType
|
||||||
|
|
||||||
private let scrollNode: ASScrollNode
|
private let scrollNode: ASScrollNode
|
||||||
private let floatingHeaderNode: FloatingHeaderNode
|
private let floatingHeaderNode: FloatingHeaderNode
|
||||||
@ -462,6 +643,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
|||||||
private let listDisposable = MetaDisposable()
|
private let listDisposable = MetaDisposable()
|
||||||
private var hiddenMediaDisposable: Disposable?
|
private var hiddenMediaDisposable: Disposable?
|
||||||
private var mediaItems: [VisualMediaItem] = []
|
private var mediaItems: [VisualMediaItem] = []
|
||||||
|
private var itemsLayout: ItemsLayout?
|
||||||
private var visibleMediaItems: [UInt32: VisualMediaItemNode] = [:]
|
private var visibleMediaItems: [UInt32: VisualMediaItemNode] = [:]
|
||||||
|
|
||||||
private var numberOfItemsToRequest: Int = 50
|
private var numberOfItemsToRequest: Int = 50
|
||||||
@ -471,10 +653,11 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
|||||||
|
|
||||||
private var decelerationAnimator: ConstantDisplayLinkAnimator?
|
private var decelerationAnimator: ConstantDisplayLinkAnimator?
|
||||||
|
|
||||||
init(context: AccountContext, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId) {
|
init(context: AccountContext, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId, contentType: ContentType) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.peerId = peerId
|
self.peerId = peerId
|
||||||
self.chatControllerInteraction = chatControllerInteraction
|
self.chatControllerInteraction = chatControllerInteraction
|
||||||
|
self.contentType = contentType
|
||||||
|
|
||||||
self.scrollNode = ASScrollNode()
|
self.scrollNode = ASScrollNode()
|
||||||
self.floatingHeaderNode = FloatingHeaderNode()
|
self.floatingHeaderNode = FloatingHeaderNode()
|
||||||
@ -536,7 +719,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.isRequestingView = true
|
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
|
|> deliverOnMainQueue).start(next: { [weak self] (view, updateType, _) in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
@ -557,6 +740,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
|||||||
for entry in view.entries.reversed() {
|
for entry in view.entries.reversed() {
|
||||||
self.mediaItems.append(VisualMediaItem(message: entry.message))
|
self.mediaItems.append(VisualMediaItem(message: entry.message))
|
||||||
}
|
}
|
||||||
|
self.itemsLayout = nil
|
||||||
|
|
||||||
let wasFirstHistoryView = self.isFirstHistoryView
|
let wasFirstHistoryView = self.isFirstHistoryView
|
||||||
self.isFirstHistoryView = false
|
self.isFirstHistoryView = false
|
||||||
@ -675,15 +859,20 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
|||||||
|
|
||||||
let availableWidth = size.width - sideInset * 2.0
|
let availableWidth = size.width - sideInset * 2.0
|
||||||
|
|
||||||
let itemSpacing: CGFloat = 1.0
|
let itemsLayout: ItemsLayout
|
||||||
let itemsInRow: Int = max(3, min(6, Int(availableWidth / 140.0)))
|
if let current = self.itemsLayout {
|
||||||
let itemSize: CGFloat = floor(availableWidth / CGFloat(itemsInRow))
|
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)
|
self.scrollNode.view.contentSize = CGSize(width: size.width, height: itemsLayout.contentHeight)
|
||||||
|
|
||||||
let contentHeight = CGFloat(rowCount + 1) * itemSpacing + CGFloat(rowCount) * itemSize + bottomInset
|
|
||||||
|
|
||||||
self.scrollNode.view.contentSize = CGSize(width: size.width, height: contentHeight)
|
|
||||||
self.updateVisibleItems(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, theme: presentationData.theme, strings: presentationData.strings, synchronousLoad: synchronous)
|
self.updateVisibleItems(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, theme: presentationData.theme, strings: presentationData.strings, synchronousLoad: synchronous)
|
||||||
|
|
||||||
if isScrollingLockedAtTop {
|
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) {
|
private func updateVisibleItems(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, theme: PresentationTheme, strings: PresentationStrings, synchronousLoad: Bool) {
|
||||||
let availableWidth = size.width - sideInset * 2.0
|
guard let itemsLayout = self.itemsLayout else {
|
||||||
|
return
|
||||||
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)
|
|
||||||
|
|
||||||
let headerItemMinY = self.scrollNode.view.bounds.minY + 20.0
|
let headerItemMinY = self.scrollNode.view.bounds.minY + 20.0
|
||||||
let visibleRect = self.scrollNode.view.bounds.insetBy(dx: 0.0, dy: -400.0)
|
let activeRect = self.scrollNode.view.bounds
|
||||||
var minVisibleRow = Int(floor((visibleRect.minY - itemSpacing) / (itemSize + itemSpacing)))
|
let visibleRect = activeRect.insetBy(dx: 0.0, dy: -400.0)
|
||||||
minVisibleRow = max(0, minVisibleRow)
|
|
||||||
var maxVisibleRow = Int(ceil((visibleRect.maxY - itemSpacing) / (itemSize + itemSpacing)))
|
|
||||||
maxVisibleRow = min(rowCount - 1, maxVisibleRow)
|
|
||||||
|
|
||||||
let minVisibleIndex = minVisibleRow * itemsInRow
|
let (minVisibleIndex, maxVisibleIndex) = itemsLayout.visibleRange(rect: visibleRect)
|
||||||
let maxVisibleIndex = min(self.mediaItems.count - 1, (maxVisibleRow + 1) * itemsInRow - 1)
|
|
||||||
|
|
||||||
var headerItem: Message?
|
var headerItem: Message?
|
||||||
|
|
||||||
@ -761,10 +942,9 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
|||||||
for i in minVisibleIndex ... maxVisibleIndex {
|
for i in minVisibleIndex ... maxVisibleIndex {
|
||||||
let stableId = self.mediaItems[i].message.stableId
|
let stableId = self.mediaItems[i].message.stableId
|
||||||
validIds.insert(stableId)
|
validIds.insert(stableId)
|
||||||
let rowIndex = i / Int(itemsInRow)
|
|
||||||
let columnIndex = i % Int(itemsInRow)
|
let itemFrame = itemsLayout.frame(forItemAt: i, sideInset: sideInset)
|
||||||
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 itemNode: VisualMediaItemNode
|
let itemNode: VisualMediaItemNode
|
||||||
if let current = self.visibleMediaItems[stableId] {
|
if let current = self.visibleMediaItems[stableId] {
|
||||||
itemNode = current
|
itemNode = current
|
||||||
@ -782,6 +962,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
|||||||
itemSynchronousLoad = synchronousLoad
|
itemSynchronousLoad = synchronousLoad
|
||||||
}
|
}
|
||||||
itemNode.update(size: itemFrame.size, item: self.mediaItems[i], theme: theme, synchronousLoad: itemSynchronousLoad)
|
itemNode.update(size: itemFrame.size, item: self.mediaItems[i], theme: theme, synchronousLoad: itemSynchronousLoad)
|
||||||
|
itemNode.updateIsVisible(itemFrame.intersects(activeRect))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var removeKeys: [UInt32] = []
|
var removeKeys: [UInt32] = []
|
||||||
@ -872,3 +1053,201 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
|||||||
return result
|
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),
|
(.file, .files),
|
||||||
(.music, .music),
|
(.music, .music),
|
||||||
(.voiceOrInstantVideo, .voice),
|
(.voiceOrInstantVideo, .voice),
|
||||||
(.webPage, .links)
|
(.webPage, .links),
|
||||||
|
(.gif, .gifs)
|
||||||
]
|
]
|
||||||
enum PaneState {
|
enum PaneState {
|
||||||
case loading
|
case loading
|
||||||
@ -174,8 +175,8 @@ enum PeerInfoMembersData: Equatable {
|
|||||||
|
|
||||||
var membersContext: PeerInfoMembersContext {
|
var membersContext: PeerInfoMembersContext {
|
||||||
switch self {
|
switch self {
|
||||||
case let .shortList(shortList):
|
case let .shortList(membersContext, _):
|
||||||
return shortList.membersContext
|
return membersContext
|
||||||
case let .longList(membersContext):
|
case let .longList(membersContext):
|
||||||
return membersContext
|
return membersContext
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,6 +53,7 @@ enum PeerInfoPaneKey {
|
|||||||
case links
|
case links
|
||||||
case voice
|
case voice
|
||||||
case music
|
case music
|
||||||
|
case gifs
|
||||||
case groupsInCommon
|
case groupsInCommon
|
||||||
case members
|
case members
|
||||||
}
|
}
|
||||||
@ -380,7 +381,7 @@ private final class PeerInfoPendingPane {
|
|||||||
let paneNode: PeerInfoPaneNode
|
let paneNode: PeerInfoPaneNode
|
||||||
switch key {
|
switch key {
|
||||||
case .media:
|
case .media:
|
||||||
paneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId)
|
paneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, contentType: .photoOrVideo)
|
||||||
case .files:
|
case .files:
|
||||||
paneNode = PeerInfoListPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .file)
|
paneNode = PeerInfoListPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .file)
|
||||||
case .links:
|
case .links:
|
||||||
@ -389,6 +390,8 @@ private final class PeerInfoPendingPane {
|
|||||||
paneNode = PeerInfoListPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .voiceOrInstantVideo)
|
paneNode = PeerInfoListPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .voiceOrInstantVideo)
|
||||||
case .music:
|
case .music:
|
||||||
paneNode = PeerInfoListPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .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:
|
case .groupsInCommon:
|
||||||
paneNode = PeerInfoGroupsInCommonPaneNode(context: context, peerId: peerId, chatControllerInteraction: chatControllerInteraction, openPeerContextAction: openPeerContextAction, groupsInCommonContext: data.groupsInCommon!)
|
paneNode = PeerInfoGroupsInCommonPaneNode(context: context, peerId: peerId, chatControllerInteraction: chatControllerInteraction, openPeerContextAction: openPeerContextAction, groupsInCommonContext: data.groupsInCommon!)
|
||||||
case .members:
|
case .members:
|
||||||
@ -838,6 +841,8 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegat
|
|||||||
title = presentationData.strings.PeerInfo_PaneLinks
|
title = presentationData.strings.PeerInfo_PaneLinks
|
||||||
case .voice:
|
case .voice:
|
||||||
title = presentationData.strings.PeerInfo_PaneVoiceAndVideo
|
title = presentationData.strings.PeerInfo_PaneVoiceAndVideo
|
||||||
|
case .gifs:
|
||||||
|
title = presentationData.strings.PeerInfo_PaneGifs
|
||||||
case .music:
|
case .music:
|
||||||
title = presentationData.strings.PeerInfo_PaneAudio
|
title = presentationData.strings.PeerInfo_PaneAudio
|
||||||
case .groupsInCommon:
|
case .groupsInCommon:
|
||||||
|
|||||||
@ -85,7 +85,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
|||||||
self.segmentedControlNode = nil
|
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()
|
super.init()
|
||||||
|
|
||||||
|
|||||||
@ -18,6 +18,7 @@ public enum UndoOverlayContent {
|
|||||||
case actionSucceeded(title: String, text: String, cancel: String)
|
case actionSucceeded(title: String, text: String, cancel: String)
|
||||||
case stickersModified(title: String, text: String, undo: Bool, info: StickerPackCollectionInfo, topItem: ItemCollectionItem?, account: Account)
|
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 dice(dice: TelegramMediaDice, account: Account, text: String, action: String?)
|
||||||
|
case chatAddedToFolder(chatTitle: String, folderTitle: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum UndoOverlayAction {
|
public enum UndoOverlayAction {
|
||||||
|
|||||||
@ -168,6 +168,22 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
|||||||
displayUndo = true
|
displayUndo = true
|
||||||
undoText = cancel
|
undoText = cancel
|
||||||
self.originalRemainingSeconds = 5
|
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):
|
case let .emoji(path, text):
|
||||||
self.iconNode = nil
|
self.iconNode = nil
|
||||||
self.iconCheckNode = nil
|
self.iconCheckNode = nil
|
||||||
@ -352,7 +368,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
|
|||||||
switch content {
|
switch content {
|
||||||
case .removedChat:
|
case .removedChat:
|
||||||
self.panelWrapperNode.addSubnode(self.timerTextNode)
|
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
|
break
|
||||||
case .dice:
|
case .dice:
|
||||||
self.panelWrapperNode.clipsToBounds = true
|
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_SecureStorageReset_Title: String { return self._s[219]! }
|
||||||
public var Wallet_Receive_CommentHeader: String { return self._s[220]! }
|
public var Wallet_Receive_CommentHeader: String { return self._s[220]! }
|
||||||
public var Wallet_Info_ReceiveGrams: String { return self._s[221]! }
|
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 form = getPluralizationForm(self.lc, value)
|
||||||
let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator)
|
let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator)
|
||||||
return String(format: self._ps[0 * 6 + Int(form.rawValue)]!, stringValue)
|
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 form = getPluralizationForm(self.lc, value)
|
||||||
let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator)
|
let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator)
|
||||||
return String(format: self._ps[1 * 6 + Int(form.rawValue)]!, stringValue)
|
return String(format: self._ps[1 * 6 + Int(form.rawValue)]!, stringValue)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user