Updates too many to describe

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

View File

@ -5342,6 +5342,7 @@ Any member of this group will be able to see messages in the channel.";
"PeerInfo.PaneAudio" = "Audio"; "PeerInfo.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$@";

View File

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

View File

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

View File

@ -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)
|> deliverOnMainQueue).start(next: { filters in
guard let strongSelf = self else {
return
}
strongSelf.push(chatListFilterAddChatsController(context: strongSelf.context, filter: filter, allFilters: filters))
f(.dismissWithoutContent) f(.dismissWithoutContent)
})
found = true found = true
break break
} }

View File

@ -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,7 +834,9 @@ 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)
|> deliverOnMainQueue).start(next: { filters in
let controller = internalChatListFilterAddChatsController(context: context, filter: filter, allFilters: filters, applyAutomatically: false, updated: { filter in
skipStateAnimation = true skipStateAnimation = true
updateState { state in updateState { state in
var state = state var state = state
@ -845,6 +847,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
} }
}) })
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
})
}, },
openAddExcludePeer: { openAddExcludePeer: {
let state = stateValue.with { $0 } let state = stateValue.with { $0 }
@ -852,7 +855,9 @@ 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)
|> deliverOnMainQueue).start(next: { filters in
let controller = internalChatListFilterExcludeChatsController(context: context, filter: filter, allFilters: filters, applyAutomatically: false, updated: { filter in
skipStateAnimation = true skipStateAnimation = true
updateState { state in updateState { state in
var state = state var state = state
@ -866,6 +871,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
} }
}) })
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
})
}, },
deleteIncludePeer: { peerId in deleteIncludePeer: { peerId in
updateState { state in updateState { state in

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1111,6 +1111,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) let 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))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -140,6 +140,21 @@ public struct ChatListFilterIncludePeers: Equatable, Hashable {
} }
} }
public mutating func addPeer(_ peerId: PeerId) -> Bool {
if self.pinnedPeers.contains(peerId) {
return false
}
if self.peers.contains(peerId) {
return false
}
if self.peers.count + self.pinnedPeers.count >= 100 {
return false
}
self.peers.insert(peerId, at: 0)
return true
}
public mutating func setPeers(_ peers: [PeerId]) { 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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -125,7 +125,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
}) })
switch self.mode { 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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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