mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-03 05:03:45 +00:00
Revert "Revert to release-7.0.1"
This reverts commit 5a32d25fc621639ac3950274b912b361d68e3908.
This commit is contained in:
parent
9e892ca79a
commit
a9c67e4210
2
Makefile
2
Makefile
@ -3,7 +3,7 @@
|
||||
include Utils.makefile
|
||||
|
||||
|
||||
APP_VERSION="7.0.1"
|
||||
APP_VERSION="7.1"
|
||||
CORE_COUNT=$(shell sysctl -n hw.logicalcpu)
|
||||
CORE_COUNT_MINUS_ONE=$(shell expr ${CORE_COUNT} \- 1)
|
||||
|
||||
|
||||
@ -108,7 +108,8 @@ dispatch_block_t fetchImage(BuildConfig *buildConfig, AccountProxyConnection * _
|
||||
apiEnvironment = [apiEnvironment withUpdatedSocksProxySettings:[[MTSocksProxySettings alloc] initWithIp:proxyConnection.host port:(uint16_t)proxyConnection.port username:proxyConnection.username password:proxyConnection.password secret:proxyConnection.secret]];
|
||||
}
|
||||
|
||||
MTContext *context = [[MTContext alloc] initWithSerialization:serialization encryptionProvider:[[EmptyEncryptionProvider alloc] init] apiEnvironment:apiEnvironment isTestingEnvironment:account.isTestingEnvironment useTempAuthKeys:false];
|
||||
MTContext *context = [[MTContext alloc] initWithSerialization:serialization encryptionProvider:[[EmptyEncryptionProvider alloc] init] apiEnvironment:apiEnvironment isTestingEnvironment:account.isTestingEnvironment useTempAuthKeys:true];
|
||||
context.tempKeyExpiration = 10 * 60 * 60;
|
||||
|
||||
NSDictionary *seedAddressList = @{};
|
||||
|
||||
@ -153,7 +154,7 @@ dispatch_block_t fetchImage(BuildConfig *buildConfig, AccountProxyConnection * _
|
||||
|
||||
for (NSNumber *datacenterId in account.datacenters) {
|
||||
AccountDatacenterInfo *info = account.datacenters[datacenterId];
|
||||
[context updateAuthInfoForDatacenterWithId:[datacenterId intValue] authInfo:[[MTDatacenterAuthInfo alloc] initWithAuthKey:info.masterKey.data authKeyId:info.masterKey.keyId saltSet:@[] authKeyAttributes:@{} mainTempAuthKey:nil mediaTempAuthKey:nil]];
|
||||
[context updateAuthInfoForDatacenterWithId:[datacenterId intValue] authInfo:[[MTDatacenterAuthInfo alloc] initWithAuthKey:info.masterKey.data authKeyId:info.masterKey.keyId saltSet:@[] authKeyAttributes:@{}] selector:MTDatacenterAuthInfoSelectorPersistent];
|
||||
}
|
||||
|
||||
MTProto *mtProto = [[MTProto alloc] initWithContext:context datacenterId:datacenterId usageCalculationInfo:nil requiredAuthToken:nil authTokenMasterDatacenterId:0];
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
@implementation Serialization
|
||||
|
||||
- (NSUInteger)currentLayer {
|
||||
return 118;
|
||||
return 119;
|
||||
}
|
||||
|
||||
- (id _Nullable)parseMessage:(NSData * _Nullable)data {
|
||||
|
||||
@ -2607,6 +2607,7 @@ Unused sets are archived when you add more.";
|
||||
"Channel.AdminLog.CanInviteUsers" = "Add Users";
|
||||
"Channel.AdminLog.CanPinMessages" = "Pin Messages";
|
||||
"Channel.AdminLog.CanAddAdmins" = "Add New Admins";
|
||||
"Channel.AdminLog.CanBeAnonymous" = "Remain Anonymous";
|
||||
"Channel.AdminLog.CanEditMessages" = "Edit Messages";
|
||||
|
||||
"Channel.AdminLog.MessageToggleInvitesOn" = "%@ enabled group invites";
|
||||
@ -5741,3 +5742,31 @@ Any member of this group will be able to see messages in the channel.";
|
||||
"AccessDenied.VideoCallCamera" = "Telegram needs access to your camera to make video calls.\n\nPlease go to Settings > Privacy > Camera and set Telegram to ON.";
|
||||
|
||||
"Call.AccountIsLoggedOnCurrentDevice" = "Sorry, you can't call %@ because that account is logged in to Telegram on the device you're using for the call.";
|
||||
|
||||
"ChatList.Search.FilterMedia" = "Media";
|
||||
"ChatList.Search.FilterLinks" = "Links";
|
||||
"ChatList.Search.FilterFiles" = "Files";
|
||||
"ChatList.Search.FilterMusic" = "Music";
|
||||
"ChatList.Search.FilterVoice" = "Voice";
|
||||
|
||||
"ChatList.Search.NoResults" = "No Results";
|
||||
"ChatList.Search.NoResultsQueryDescription" = "There were no results for \"%@\".\nTry a new search.";
|
||||
"ChatList.Search.NoResultsDescription" = "There were no results.\nTry a new search.";
|
||||
|
||||
"ChatList.Search.NoResultsFilter" = "Nothing Yet";
|
||||
"ChatList.Search.NoResultsFitlerMedia" = "Photos and videos from all your chats will be shown here.";
|
||||
"ChatList.Search.NoResultsFitlerLinks" = "Links from all your chats will be shown here.";
|
||||
"ChatList.Search.NoResultsFitlerFiles" = "Files from all your chats will be shown here.";
|
||||
"ChatList.Search.NoResultsFitlerMusic" = "Music from all your chats will be shown here.";
|
||||
"ChatList.Search.NoResultsFitlerVoice" = "Voice and video messages from all your chats will be shown here.";
|
||||
|
||||
"ChatList.Search.Messages_0" = "%@ messages";
|
||||
"ChatList.Search.Messages_1" = "%@ message";
|
||||
"ChatList.Search.Messages_2" = "%@ messages";
|
||||
"ChatList.Search.Messages_3_10" = "%@ messages";
|
||||
"ChatList.Search.Messages_many" = "%@ messages";
|
||||
"ChatList.Search.Messages_any" = "%@ messages";
|
||||
|
||||
"Conversation.InputTextAnonymousPlaceholder" = "Send anonymously";
|
||||
|
||||
"DialogList.Replies" = "Replies";
|
||||
|
||||
5793
Telegram/Telegram-iOS/en.lproj/Localizable.strings.orig
Normal file
5793
Telegram/Telegram-iOS/en.lproj/Localizable.strings.orig
Normal file
File diff suppressed because it is too large
Load Diff
@ -170,6 +170,7 @@ public enum ResolvedUrl {
|
||||
case botStart(peerId: PeerId, payload: String)
|
||||
case groupBotStart(peerId: PeerId, payload: String)
|
||||
case channelMessage(peerId: PeerId, messageId: MessageId)
|
||||
case replyThreadMessage(replyThreadMessageId: MessageId, isChannelPost: Bool, maxReadMessageId: MessageId?, messageId: MessageId)
|
||||
case stickerPack(name: String)
|
||||
case instantView(TelegramMediaWebpage, String?)
|
||||
case proxy(host: String, port: Int32, username: String?, password: String?, secret: Data?)
|
||||
@ -249,6 +250,12 @@ public enum ChatSearchDomain: Equatable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum ChatLocation: Equatable {
|
||||
case peer(PeerId)
|
||||
case replyThread(threadMessageId: MessageId, isChannelPost: Bool, maxReadMessageId: MessageId?)
|
||||
}
|
||||
|
||||
public final class NavigateToChatControllerParams {
|
||||
public let navigationController: NavigationController
|
||||
public let chatController: ChatController?
|
||||
@ -522,8 +529,8 @@ public protocol SharedAccountContext: class {
|
||||
func handleTextLinkAction(context: AccountContext, peerId: PeerId?, navigateDisposable: MetaDisposable, controller: ViewController, action: TextLinkItemActionType, itemLink: TextLinkItem)
|
||||
func navigateToChat(accountId: AccountRecordId, peerId: PeerId, messageId: MessageId?)
|
||||
func openChatMessage(_ params: OpenChatMessageParams) -> Bool
|
||||
func messageFromPreloadedChatHistoryViewForLocation(id: MessageId, location: ChatHistoryLocationInput, account: Account, chatLocation: ChatLocation, tagMask: MessageTags?) -> Signal<(MessageIndex?, Bool), NoError>
|
||||
func makeOverlayAudioPlayerController(context: AccountContext, peerId: PeerId, type: MediaManagerPlayerType, initialMessageId: MessageId, initialOrder: MusicPlaybackSettingsOrder, parentNavigationController: NavigationController?) -> ViewController & OverlayAudioPlayerController
|
||||
func messageFromPreloadedChatHistoryViewForLocation(id: MessageId, location: ChatHistoryLocationInput, context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, tagMask: MessageTags?) -> Signal<(MessageIndex?, Bool), NoError>
|
||||
func makeOverlayAudioPlayerController(context: AccountContext, peerId: PeerId, type: MediaManagerPlayerType, initialMessageId: MessageId, initialOrder: MusicPlaybackSettingsOrder, isGlobalSearch: Bool, parentNavigationController: NavigationController?) -> ViewController & OverlayAudioPlayerController
|
||||
func makePeerInfoController(context: AccountContext, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, fromChat: Bool) -> ViewController?
|
||||
func makeChannelAdminController(context: AccountContext, peerId: PeerId, adminId: PeerId, initialParticipant: ChannelParticipant) -> ViewController?
|
||||
func makeDeviceContactInfoController(context: AccountContext, subject: DeviceContactInfoSubject, completed: (() -> Void)?, cancelled: (() -> Void)?) -> ViewController
|
||||
@ -632,6 +639,9 @@ public final class TonContext {
|
||||
|
||||
#endif
|
||||
|
||||
public protocol ChatLocationContextHolder: class {
|
||||
}
|
||||
|
||||
public protocol AccountContext: class {
|
||||
var sharedContext: SharedAccountContext { get }
|
||||
var account: Account { get }
|
||||
@ -659,4 +669,7 @@ public protocol AccountContext: class {
|
||||
|
||||
func storeSecureIdPassword(password: String)
|
||||
func getStoredSecureIdPassword() -> String?
|
||||
|
||||
func chatLocationInput(for location: ChatLocation, contextHolder: Atomic<ChatLocationContextHolder?>) -> ChatLocationInput
|
||||
func applyMaxReadIndex(for location: ChatLocation, contextHolder: Atomic<ChatLocationContextHolder?>, messageIndex: MessageIndex)
|
||||
}
|
||||
|
||||
@ -10,6 +10,93 @@ import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
|
||||
public final class ChatMessageItemAssociatedData: Equatable {
|
||||
public enum ChannelDiscussionGroupStatus: Equatable {
|
||||
case unknown
|
||||
case known(PeerId?)
|
||||
}
|
||||
|
||||
public let automaticDownloadPeerType: MediaAutoDownloadPeerType
|
||||
public let automaticDownloadNetworkType: MediaAutoDownloadNetworkType
|
||||
public let isRecentActions: Bool
|
||||
public let isScheduledMessages: Bool
|
||||
public let contactsPeerIds: Set<PeerId>
|
||||
public let channelDiscussionGroup: ChannelDiscussionGroupStatus
|
||||
public let animatedEmojiStickers: [String: [StickerPackItem]]
|
||||
public let forcedResourceStatus: FileMediaResourceStatus?
|
||||
|
||||
public init(automaticDownloadPeerType: MediaAutoDownloadPeerType, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, isRecentActions: Bool = false, isScheduledMessages: Bool = false, contactsPeerIds: Set<PeerId> = Set(), channelDiscussionGroup: ChannelDiscussionGroupStatus = .unknown, animatedEmojiStickers: [String: [StickerPackItem]] = [:], forcedResourceStatus: FileMediaResourceStatus? = nil) {
|
||||
self.automaticDownloadPeerType = automaticDownloadPeerType
|
||||
self.automaticDownloadNetworkType = automaticDownloadNetworkType
|
||||
self.isRecentActions = isRecentActions
|
||||
self.isScheduledMessages = isScheduledMessages
|
||||
self.contactsPeerIds = contactsPeerIds
|
||||
self.channelDiscussionGroup = channelDiscussionGroup
|
||||
self.animatedEmojiStickers = animatedEmojiStickers
|
||||
self.forcedResourceStatus = forcedResourceStatus
|
||||
}
|
||||
|
||||
public static func == (lhs: ChatMessageItemAssociatedData, rhs: ChatMessageItemAssociatedData) -> Bool {
|
||||
if lhs.automaticDownloadPeerType != rhs.automaticDownloadPeerType {
|
||||
return false
|
||||
}
|
||||
if lhs.automaticDownloadNetworkType != rhs.automaticDownloadNetworkType {
|
||||
return false
|
||||
}
|
||||
if lhs.isRecentActions != rhs.isRecentActions {
|
||||
return false
|
||||
}
|
||||
if lhs.isScheduledMessages != rhs.isScheduledMessages {
|
||||
return false
|
||||
}
|
||||
if lhs.contactsPeerIds != rhs.contactsPeerIds {
|
||||
return false
|
||||
}
|
||||
if lhs.channelDiscussionGroup != rhs.channelDiscussionGroup {
|
||||
return false
|
||||
}
|
||||
if lhs.animatedEmojiStickers != rhs.animatedEmojiStickers {
|
||||
return false
|
||||
}
|
||||
if lhs.forcedResourceStatus != rhs.forcedResourceStatus {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public enum ChatControllerInteractionLongTapAction {
|
||||
case url(String)
|
||||
case mention(String)
|
||||
case peerMention(PeerId, String)
|
||||
case command(String)
|
||||
case hashtag(String)
|
||||
case timecode(Double, String)
|
||||
case bankCard(String)
|
||||
}
|
||||
|
||||
public enum ChatHistoryMessageSelection: Equatable {
|
||||
case none
|
||||
case selectable(selected: Bool)
|
||||
|
||||
public static func ==(lhs: ChatHistoryMessageSelection, rhs: ChatHistoryMessageSelection) -> Bool {
|
||||
switch lhs {
|
||||
case .none:
|
||||
if case .none = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .selectable(selected):
|
||||
if case .selectable(selected) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum ChatControllerInitialBotStartBehavior {
|
||||
case interactive
|
||||
case automatic(returnToPeerId: PeerId, scheduled: Bool)
|
||||
|
||||
@ -15,8 +15,8 @@ public enum ChatHistoryLocation: Equatable {
|
||||
}
|
||||
|
||||
public struct ChatHistoryLocationInput: Equatable {
|
||||
public let content: ChatHistoryLocation
|
||||
public let id: Int32
|
||||
public var content: ChatHistoryLocation
|
||||
public var id: Int32
|
||||
|
||||
public init(content: ChatHistoryLocation, id: Int32) {
|
||||
self.content = content
|
||||
|
||||
@ -1,5 +1,13 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
|
||||
public enum GalleryControllerItemSource {
|
||||
case peerMessagesAtId(messageId: MessageId, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>)
|
||||
case standaloneMessage(Message)
|
||||
case custom(messages: Signal<([Message], Int32, Bool), NoError>, messageId: MessageId, loadMore: (() -> Void)?)
|
||||
}
|
||||
|
||||
public final class GalleryControllerActionInteraction {
|
||||
public let openUrl: (String, Bool) -> Void
|
||||
|
||||
@ -8,6 +8,120 @@ import AsyncDisplayKit
|
||||
import TelegramAudio
|
||||
import UniversalMediaPlayer
|
||||
|
||||
public enum PeerMessagesMediaPlaylistId: Equatable, SharedMediaPlaylistId {
|
||||
case peer(PeerId)
|
||||
case recentActions(PeerId)
|
||||
case custom
|
||||
|
||||
public func isEqual(to: SharedMediaPlaylistId) -> Bool {
|
||||
if let to = to as? PeerMessagesMediaPlaylistId {
|
||||
return self == to
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public enum PeerMessagesPlaylistLocation: Equatable, SharedMediaPlaylistLocation {
|
||||
case messages(peerId: PeerId, tagMask: MessageTags, at: MessageId)
|
||||
case singleMessage(MessageId)
|
||||
case recentActions(Message)
|
||||
case custom(messages: Signal<([Message], Int32, Bool), NoError>, at: MessageId, loadMore: (() -> Void)?)
|
||||
|
||||
public var playlistId: PeerMessagesMediaPlaylistId {
|
||||
switch self {
|
||||
case let .messages(peerId, _, _):
|
||||
return .peer(peerId)
|
||||
case let .singleMessage(id):
|
||||
return .peer(id.peerId)
|
||||
case let .recentActions(message):
|
||||
return .recentActions(message.id.peerId)
|
||||
case .custom:
|
||||
return .custom
|
||||
}
|
||||
}
|
||||
|
||||
public var messageId: MessageId? {
|
||||
switch self {
|
||||
case let .messages(_, _, messageId), let .singleMessage(messageId), let .custom(_, messageId, _):
|
||||
return messageId
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func isEqual(to: SharedMediaPlaylistLocation) -> Bool {
|
||||
if let to = to as? PeerMessagesPlaylistLocation {
|
||||
return self == to
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public static func ==(lhs: PeerMessagesPlaylistLocation, rhs: PeerMessagesPlaylistLocation) -> Bool {
|
||||
switch lhs {
|
||||
case let .messages(peerId, tagMask, at):
|
||||
if case .messages(peerId, tagMask, at) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .singleMessage(messageId):
|
||||
if case .singleMessage(messageId) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .recentActions(lhsMessage):
|
||||
if case let .recentActions(rhsMessage) = rhs, lhsMessage.id == rhsMessage.id {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .custom(_, lhsAt, _):
|
||||
if case let .custom(_, rhsAt, _) = rhs, lhsAt == rhsAt {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func peerMessageMediaPlayerType(_ message: Message) -> MediaManagerPlayerType? {
|
||||
func extractFileMedia(_ message: Message) -> TelegramMediaFile? {
|
||||
var file: TelegramMediaFile?
|
||||
for media in message.media {
|
||||
if let media = media as? TelegramMediaFile {
|
||||
file = media
|
||||
break
|
||||
} else if let media = media as? TelegramMediaWebpage, case let .Loaded(content) = media.content, let f = content.file {
|
||||
file = f
|
||||
break
|
||||
}
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
if let file = extractFileMedia(message) {
|
||||
if file.isVoice || file.isInstantVideo {
|
||||
return .voice
|
||||
} else if file.isMusic {
|
||||
return .music
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func peerMessagesMediaPlaylistAndItemId(_ message: Message, isRecentActions: Bool, isGlobalSearch: Bool) -> (SharedMediaPlaylistId, SharedMediaPlaylistItemId)? {
|
||||
if isGlobalSearch {
|
||||
return (PeerMessagesMediaPlaylistId.custom, PeerMessagesMediaPlaylistItemId(messageId: message.id))
|
||||
} else if isRecentActions {
|
||||
return (PeerMessagesMediaPlaylistId.recentActions(message.id.peerId), PeerMessagesMediaPlaylistItemId(messageId: message.id))
|
||||
} else {
|
||||
return (PeerMessagesMediaPlaylistId.peer(message.id.peerId), PeerMessagesMediaPlaylistItemId(messageId: message.id))
|
||||
}
|
||||
}
|
||||
|
||||
public enum MediaManagerPlayerType {
|
||||
case voice
|
||||
case music
|
||||
|
||||
@ -6,6 +6,7 @@ import SyncCore
|
||||
import SwiftSignalKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import UniversalMediaPlayer
|
||||
|
||||
public enum ChatControllerInteractionOpenMessageMode {
|
||||
case `default`
|
||||
@ -18,6 +19,8 @@ public enum ChatControllerInteractionOpenMessageMode {
|
||||
|
||||
public final class OpenChatMessageParams {
|
||||
public let context: AccountContext
|
||||
public let chatLocation: ChatLocation?
|
||||
public let chatLocationContextHolder: Atomic<ChatLocationContextHolder?>?
|
||||
public let message: Message
|
||||
public let standalone: Bool
|
||||
public let reverseMessageGalleryOrder: Bool
|
||||
@ -36,9 +39,13 @@ public final class OpenChatMessageParams {
|
||||
public let setupTemporaryHiddenMedia: (Signal<Any?, NoError>, Int, Media) -> Void
|
||||
public let chatAvatarHiddenMedia: (Signal<MessageId?, NoError>, Media) -> Void
|
||||
public let actionInteraction: GalleryControllerActionInteraction?
|
||||
public let playlistLocation: PeerMessagesPlaylistLocation?
|
||||
public let gallerySource: GalleryControllerItemSource?
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
chatLocation: ChatLocation?,
|
||||
chatLocationContextHolder: Atomic<ChatLocationContextHolder?>?,
|
||||
message: Message,
|
||||
standalone: Bool,
|
||||
reverseMessageGalleryOrder: Bool,
|
||||
@ -56,9 +63,13 @@ public final class OpenChatMessageParams {
|
||||
sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?,
|
||||
setupTemporaryHiddenMedia: @escaping (Signal<Any?, NoError>, Int, Media) -> Void,
|
||||
chatAvatarHiddenMedia: @escaping (Signal<MessageId?, NoError>, Media) -> Void,
|
||||
actionInteraction: GalleryControllerActionInteraction? = nil
|
||||
actionInteraction: GalleryControllerActionInteraction? = nil,
|
||||
playlistLocation: PeerMessagesPlaylistLocation? = nil,
|
||||
gallerySource: GalleryControllerItemSource? = nil
|
||||
) {
|
||||
self.context = context
|
||||
self.chatLocation = chatLocation
|
||||
self.chatLocationContextHolder = chatLocationContextHolder
|
||||
self.message = message
|
||||
self.standalone = standalone
|
||||
self.reverseMessageGalleryOrder = reverseMessageGalleryOrder
|
||||
@ -77,5 +88,7 @@ public final class OpenChatMessageParams {
|
||||
self.setupTemporaryHiddenMedia = setupTemporaryHiddenMedia
|
||||
self.chatAvatarHiddenMedia = chatAvatarHiddenMedia
|
||||
self.actionInteraction = actionInteraction
|
||||
self.playlistLocation = playlistLocation
|
||||
self.gallerySource = gallerySource
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ public func storedMessageFromSearch(account: Account, message: Message) -> Signa
|
||||
}
|
||||
}
|
||||
|
||||
let storeMessage = StoreMessage(id: .Id(message.id), globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, timestamp: message.timestamp, flags: StoreMessageFlags(message.flags), tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, forwardInfo: message.forwardInfo.flatMap(StoreMessageForwardInfo.init), authorId: message.author?.id, text: message.text, attributes: message.attributes, media: message.media)
|
||||
let storeMessage = StoreMessage(id: .Id(message.id), globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, threadId: message.threadId, timestamp: message.timestamp, flags: StoreMessageFlags(message.flags), tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, forwardInfo: message.forwardInfo.flatMap(StoreMessageForwardInfo.init), authorId: message.author?.id, text: message.text, attributes: message.attributes, media: message.media)
|
||||
|
||||
let _ = transaction.addMessages([storeMessage], location: .Random)
|
||||
}
|
||||
@ -46,7 +46,7 @@ public func storeMessageFromSearch(transaction: Transaction, message: Message) {
|
||||
}
|
||||
}
|
||||
|
||||
let storeMessage = StoreMessage(id: .Id(message.id), globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, timestamp: message.timestamp, flags: StoreMessageFlags(message.flags), tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, forwardInfo: message.forwardInfo.flatMap(StoreMessageForwardInfo.init), authorId: message.author?.id, text: message.text, attributes: message.attributes, media: message.media)
|
||||
let storeMessage = StoreMessage(id: .Id(message.id), globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, threadId: message.threadId, timestamp: message.timestamp, flags: StoreMessageFlags(message.flags), tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, forwardInfo: message.forwardInfo.flatMap(StoreMessageForwardInfo.init), authorId: message.author?.id, text: message.text, attributes: message.attributes, media: message.media)
|
||||
|
||||
let _ = transaction.addMessages([storeMessage], location: .Random)
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ import Emoji
|
||||
private let deletedIcon = UIImage(bundleImageName: "Avatar/DeletedIcon")?.precomposed()
|
||||
private let savedMessagesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/SavedMessagesIcon"), color: .white)
|
||||
private let archivedChatsIcon = UIImage(bundleImageName: "Avatar/ArchiveAvatarIcon")?.precomposed()
|
||||
private let repliesIcon = generateTintedImage(image: UIImage(bundleImageName: "Avatar/RepliesMessagesIcon"), color: .white)
|
||||
|
||||
public func avatarPlaceholderFont(size: CGFloat) -> UIFont {
|
||||
return Font.with(size: size, design: .round, traits: [.bold])
|
||||
@ -100,6 +101,7 @@ private func ==(lhs: AvatarNodeState, rhs: AvatarNodeState) -> Bool {
|
||||
private enum AvatarNodeIcon: Equatable {
|
||||
case none
|
||||
case savedMessagesIcon
|
||||
case repliesIcon
|
||||
case archivedChatsIcon(hiddenByDefault: Bool)
|
||||
case editAvatarIcon
|
||||
case deletedIcon
|
||||
@ -109,6 +111,7 @@ public enum AvatarNodeImageOverride: Equatable {
|
||||
case none
|
||||
case image(TelegramMediaImageRepresentation)
|
||||
case savedMessagesIcon
|
||||
case repliesIcon
|
||||
case archivedChatsIcon(hiddenByDefault: Bool)
|
||||
case editAvatarIcon
|
||||
case deletedIcon
|
||||
@ -308,6 +311,9 @@ public final class AvatarNode: ASDisplayNode {
|
||||
case .savedMessagesIcon:
|
||||
representation = nil
|
||||
icon = .savedMessagesIcon
|
||||
case .repliesIcon:
|
||||
representation = nil
|
||||
icon = .repliesIcon
|
||||
case let .archivedChatsIcon(hiddenByDefault):
|
||||
representation = nil
|
||||
icon = .archivedChatsIcon(hiddenByDefault: hiddenByDefault)
|
||||
@ -452,6 +458,8 @@ public final class AvatarNode: ASDisplayNode {
|
||||
colorsArray = grayscaleColors
|
||||
} else if case .savedMessagesIcon = parameters.icon {
|
||||
colorsArray = savedMessagesColors
|
||||
} else if case .repliesIcon = parameters.icon {
|
||||
colorsArray = savedMessagesColors
|
||||
} else if case .editAvatarIcon = parameters.icon, let theme = parameters.theme {
|
||||
colorsArray = [theme.list.itemAccentColor.withAlphaComponent(0.1).cgColor, theme.list.itemAccentColor.withAlphaComponent(0.1).cgColor]
|
||||
} else if case let .archivedChatsIcon(hiddenByDefault) = parameters.icon, let theme = parameters.theme {
|
||||
@ -506,6 +514,15 @@ public final class AvatarNode: ASDisplayNode {
|
||||
if let savedMessagesIcon = savedMessagesIcon {
|
||||
context.draw(savedMessagesIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - savedMessagesIcon.size.width) / 2.0), y: floor((bounds.size.height - savedMessagesIcon.size.height) / 2.0)), size: savedMessagesIcon.size))
|
||||
}
|
||||
} else if case .repliesIcon = parameters.icon {
|
||||
let factor = bounds.size.width / 60.0
|
||||
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
|
||||
context.scaleBy(x: factor, y: -factor)
|
||||
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
|
||||
|
||||
if let repliesIcon = repliesIcon {
|
||||
context.draw(repliesIcon.cgImage!, in: CGRect(origin: CGPoint(x: floor((bounds.size.width - repliesIcon.size.width) / 2.0), y: floor((bounds.size.height - repliesIcon.size.height) / 2.0)), size: repliesIcon.size))
|
||||
}
|
||||
} else if case .editAvatarIcon = parameters.icon, let theme = parameters.theme, !parameters.hasImage {
|
||||
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
|
||||
context.scaleBy(x: 1.0, y: -1.0)
|
||||
|
||||
22
submodules/ChatInterfaceState/BUCK
Normal file
22
submodules/ChatInterfaceState/BUCK
Normal file
@ -0,0 +1,22 @@
|
||||
load("//Config:buck_rule_macros.bzl", "static_library")
|
||||
|
||||
static_library(
|
||||
name = "ChatInterfaceState",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit#shared",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit#shared",
|
||||
"//submodules/Display:Display#shared",
|
||||
"//submodules/Postbox:Postbox#shared",
|
||||
"//submodules/TelegramCore:TelegramCore#shared",
|
||||
"//submodules/SyncCore:SyncCore#shared",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
],
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||
"$SDKROOT/System/Library/Frameworks/UIKit.framework",
|
||||
],
|
||||
)
|
||||
22
submodules/ChatInterfaceState/BUILD
Normal file
22
submodules/ChatInterfaceState/BUILD
Normal file
@ -0,0 +1,22 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ChatInterfaceState",
|
||||
module_name = "ChatInterfaceState",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/SyncCore:SyncCore",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
@ -6,18 +6,23 @@ import SyncCore
|
||||
import TextFormat
|
||||
import AccountContext
|
||||
|
||||
struct ChatInterfaceSelectionState: PostboxCoding, Equatable {
|
||||
let selectedIds: Set<MessageId>
|
||||
public enum ChatTextInputMediaRecordingButtonMode: Int32 {
|
||||
case audio = 0
|
||||
case video = 1
|
||||
}
|
||||
|
||||
public struct ChatInterfaceSelectionState: PostboxCoding, Equatable {
|
||||
public let selectedIds: Set<MessageId>
|
||||
|
||||
static func ==(lhs: ChatInterfaceSelectionState, rhs: ChatInterfaceSelectionState) -> Bool {
|
||||
public static func ==(lhs: ChatInterfaceSelectionState, rhs: ChatInterfaceSelectionState) -> Bool {
|
||||
return lhs.selectedIds == rhs.selectedIds
|
||||
}
|
||||
|
||||
init(selectedIds: Set<MessageId>) {
|
||||
public init(selectedIds: Set<MessageId>) {
|
||||
self.selectedIds = selectedIds
|
||||
}
|
||||
|
||||
init(decoder: PostboxDecoder) {
|
||||
public init(decoder: PostboxDecoder) {
|
||||
if let data = decoder.decodeBytesForKeyNoCopy("i") {
|
||||
self.selectedIds = Set(MessageId.decodeArrayFromBuffer(data))
|
||||
} else {
|
||||
@ -25,27 +30,27 @@ struct ChatInterfaceSelectionState: PostboxCoding, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
func encode(_ encoder: PostboxEncoder) {
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
let buffer = WriteBuffer()
|
||||
MessageId.encodeArrayToBuffer(Array(selectedIds), buffer: buffer)
|
||||
encoder.encodeBytes(buffer, forKey: "i")
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatEditMessageState: PostboxCoding, Equatable {
|
||||
let messageId: MessageId
|
||||
let inputState: ChatTextInputState
|
||||
let disableUrlPreview: String?
|
||||
let inputTextMaxLength: Int32?
|
||||
public struct ChatEditMessageState: PostboxCoding, Equatable {
|
||||
public let messageId: MessageId
|
||||
public let inputState: ChatTextInputState
|
||||
public let disableUrlPreview: String?
|
||||
public let inputTextMaxLength: Int32?
|
||||
|
||||
init(messageId: MessageId, inputState: ChatTextInputState, disableUrlPreview: String?, inputTextMaxLength: Int32?) {
|
||||
public init(messageId: MessageId, inputState: ChatTextInputState, disableUrlPreview: String?, inputTextMaxLength: Int32?) {
|
||||
self.messageId = messageId
|
||||
self.inputState = inputState
|
||||
self.disableUrlPreview = disableUrlPreview
|
||||
self.inputTextMaxLength = inputTextMaxLength
|
||||
}
|
||||
|
||||
init(decoder: PostboxDecoder) {
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.messageId = MessageId(peerId: PeerId(decoder.decodeInt64ForKey("mp", orElse: 0)), namespace: decoder.decodeInt32ForKey("mn", orElse: 0), id: decoder.decodeInt32ForKey("mi", orElse: 0))
|
||||
if let inputState = decoder.decodeObjectForKey("is", decoder: { return ChatTextInputState(decoder: $0) }) as? ChatTextInputState {
|
||||
self.inputState = inputState
|
||||
@ -56,7 +61,7 @@ struct ChatEditMessageState: PostboxCoding, Equatable {
|
||||
self.inputTextMaxLength = decoder.decodeOptionalInt32ForKey("tl")
|
||||
}
|
||||
|
||||
func encode(_ encoder: PostboxEncoder) {
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt64(self.messageId.peerId.toInt64(), forKey: "mp")
|
||||
encoder.encodeInt32(self.messageId.namespace, forKey: "mn")
|
||||
encoder.encodeInt32(self.messageId.id, forKey: "mi")
|
||||
@ -73,31 +78,31 @@ struct ChatEditMessageState: PostboxCoding, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: ChatEditMessageState, rhs: ChatEditMessageState) -> Bool {
|
||||
public static func ==(lhs: ChatEditMessageState, rhs: ChatEditMessageState) -> Bool {
|
||||
return lhs.messageId == rhs.messageId && lhs.inputState == rhs.inputState && lhs.disableUrlPreview == rhs.disableUrlPreview && lhs.inputTextMaxLength == rhs.inputTextMaxLength
|
||||
}
|
||||
|
||||
func withUpdatedInputState(_ inputState: ChatTextInputState) -> ChatEditMessageState {
|
||||
public func withUpdatedInputState(_ inputState: ChatTextInputState) -> ChatEditMessageState {
|
||||
return ChatEditMessageState(messageId: self.messageId, inputState: inputState, disableUrlPreview: self.disableUrlPreview, inputTextMaxLength: self.inputTextMaxLength)
|
||||
}
|
||||
|
||||
func withUpdatedDisableUrlPreview(_ disableUrlPreview: String?) -> ChatEditMessageState {
|
||||
public func withUpdatedDisableUrlPreview(_ disableUrlPreview: String?) -> ChatEditMessageState {
|
||||
return ChatEditMessageState(messageId: self.messageId, inputState: self.inputState, disableUrlPreview: disableUrlPreview, inputTextMaxLength: self.inputTextMaxLength)
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatInterfaceMessageActionsState: PostboxCoding, Equatable {
|
||||
var closedButtonKeyboardMessageId: MessageId?
|
||||
var processedSetupReplyMessageId: MessageId?
|
||||
var closedPinnedMessageId: MessageId?
|
||||
var closedPeerSpecificPackSetup: Bool = false
|
||||
var dismissedAddContactPhoneNumber: String?
|
||||
public struct ChatInterfaceMessageActionsState: PostboxCoding, Equatable {
|
||||
public var closedButtonKeyboardMessageId: MessageId?
|
||||
public var processedSetupReplyMessageId: MessageId?
|
||||
public var closedPinnedMessageId: MessageId?
|
||||
public var closedPeerSpecificPackSetup: Bool = false
|
||||
public var dismissedAddContactPhoneNumber: String?
|
||||
|
||||
var isEmpty: Bool {
|
||||
public var isEmpty: Bool {
|
||||
return self.closedButtonKeyboardMessageId == nil && self.processedSetupReplyMessageId == nil && self.closedPinnedMessageId == nil && self.closedPeerSpecificPackSetup == false && self.dismissedAddContactPhoneNumber == nil
|
||||
}
|
||||
|
||||
init() {
|
||||
public init() {
|
||||
self.closedButtonKeyboardMessageId = nil
|
||||
self.processedSetupReplyMessageId = nil
|
||||
self.closedPinnedMessageId = nil
|
||||
@ -105,7 +110,7 @@ struct ChatInterfaceMessageActionsState: PostboxCoding, Equatable {
|
||||
self.dismissedAddContactPhoneNumber = nil
|
||||
}
|
||||
|
||||
init(closedButtonKeyboardMessageId: MessageId?, processedSetupReplyMessageId: MessageId?, closedPinnedMessageId: MessageId?, closedPeerSpecificPackSetup: Bool, dismissedAddContactPhoneNumber: String?) {
|
||||
public init(closedButtonKeyboardMessageId: MessageId?, processedSetupReplyMessageId: MessageId?, closedPinnedMessageId: MessageId?, closedPeerSpecificPackSetup: Bool, dismissedAddContactPhoneNumber: String?) {
|
||||
self.closedButtonKeyboardMessageId = closedButtonKeyboardMessageId
|
||||
self.processedSetupReplyMessageId = processedSetupReplyMessageId
|
||||
self.closedPinnedMessageId = closedPinnedMessageId
|
||||
@ -113,7 +118,7 @@ struct ChatInterfaceMessageActionsState: PostboxCoding, Equatable {
|
||||
self.dismissedAddContactPhoneNumber = dismissedAddContactPhoneNumber
|
||||
}
|
||||
|
||||
init(decoder: PostboxDecoder) {
|
||||
public init(decoder: PostboxDecoder) {
|
||||
if let closedMessageIdPeerId = decoder.decodeOptionalInt64ForKey("cb.p"), let closedMessageIdNamespace = decoder.decodeOptionalInt32ForKey("cb.n"), let closedMessageIdId = decoder.decodeOptionalInt32ForKey("cb.i") {
|
||||
self.closedButtonKeyboardMessageId = MessageId(peerId: PeerId(closedMessageIdPeerId), namespace: closedMessageIdNamespace, id: closedMessageIdId)
|
||||
} else {
|
||||
@ -135,7 +140,7 @@ struct ChatInterfaceMessageActionsState: PostboxCoding, Equatable {
|
||||
self.closedPeerSpecificPackSetup = decoder.decodeInt32ForKey("cpss", orElse: 0) != 0
|
||||
}
|
||||
|
||||
func encode(_ encoder: PostboxEncoder) {
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
if let closedButtonKeyboardMessageId = self.closedButtonKeyboardMessageId {
|
||||
encoder.encodeInt64(closedButtonKeyboardMessageId.peerId.toInt64(), forKey: "cb.p")
|
||||
encoder.encodeInt32(closedButtonKeyboardMessageId.namespace, forKey: "cb.n")
|
||||
@ -176,21 +181,21 @@ struct ChatInterfaceMessageActionsState: PostboxCoding, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatInterfaceHistoryScrollState: PostboxCoding, Equatable {
|
||||
let messageIndex: MessageIndex
|
||||
let relativeOffset: Double
|
||||
public struct ChatInterfaceHistoryScrollState: PostboxCoding, Equatable {
|
||||
public let messageIndex: MessageIndex
|
||||
public let relativeOffset: Double
|
||||
|
||||
init(messageIndex: MessageIndex, relativeOffset: Double) {
|
||||
public init(messageIndex: MessageIndex, relativeOffset: Double) {
|
||||
self.messageIndex = messageIndex
|
||||
self.relativeOffset = relativeOffset
|
||||
}
|
||||
|
||||
init(decoder: PostboxDecoder) {
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.messageIndex = MessageIndex(id: MessageId(peerId: PeerId(decoder.decodeInt64ForKey("m.p", orElse: 0)), namespace: decoder.decodeInt32ForKey("m.n", orElse: 0), id: decoder.decodeInt32ForKey("m.i", orElse: 0)), timestamp: decoder.decodeInt32ForKey("m.t", orElse: 0))
|
||||
self.relativeOffset = decoder.decodeDoubleForKey("ro", orElse: 0.0)
|
||||
}
|
||||
|
||||
func encode(_ encoder: PostboxEncoder) {
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt32(self.messageIndex.timestamp, forKey: "m.t")
|
||||
encoder.encodeInt64(self.messageIndex.id.peerId.toInt64(), forKey: "m.p")
|
||||
encoder.encodeInt32(self.messageIndex.id.namespace, forKey: "m.n")
|
||||
@ -198,7 +203,7 @@ struct ChatInterfaceHistoryScrollState: PostboxCoding, Equatable {
|
||||
encoder.encodeDouble(self.relativeOffset, forKey: "ro")
|
||||
}
|
||||
|
||||
static func ==(lhs: ChatInterfaceHistoryScrollState, rhs: ChatInterfaceHistoryScrollState) -> Bool {
|
||||
public static func ==(lhs: ChatInterfaceHistoryScrollState, rhs: ChatInterfaceHistoryScrollState) -> Bool {
|
||||
if lhs.messageIndex != rhs.messageIndex {
|
||||
return false
|
||||
}
|
||||
@ -210,18 +215,18 @@ struct ChatInterfaceHistoryScrollState: PostboxCoding, Equatable {
|
||||
}
|
||||
|
||||
public final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable {
|
||||
let timestamp: Int32
|
||||
let composeInputState: ChatTextInputState
|
||||
let composeDisableUrlPreview: String?
|
||||
let replyMessageId: MessageId?
|
||||
let forwardMessageIds: [MessageId]?
|
||||
let editMessage: ChatEditMessageState?
|
||||
let selectionState: ChatInterfaceSelectionState?
|
||||
let messageActionsState: ChatInterfaceMessageActionsState
|
||||
let historyScrollState: ChatInterfaceHistoryScrollState?
|
||||
let mediaRecordingMode: ChatTextInputMediaRecordingButtonMode
|
||||
let silentPosting: Bool
|
||||
let inputLanguage: String?
|
||||
public let timestamp: Int32
|
||||
public let composeInputState: ChatTextInputState
|
||||
public let composeDisableUrlPreview: String?
|
||||
public let replyMessageId: MessageId?
|
||||
public let forwardMessageIds: [MessageId]?
|
||||
public let editMessage: ChatEditMessageState?
|
||||
public let selectionState: ChatInterfaceSelectionState?
|
||||
public let messageActionsState: ChatInterfaceMessageActionsState
|
||||
public let historyScrollState: ChatInterfaceHistoryScrollState?
|
||||
public let mediaRecordingMode: ChatTextInputMediaRecordingButtonMode
|
||||
public let silentPosting: Bool
|
||||
public let inputLanguage: String?
|
||||
|
||||
public var associatedMessageIds: [MessageId] {
|
||||
var ids: [MessageId] = []
|
||||
@ -259,7 +264,7 @@ public final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equata
|
||||
return result
|
||||
}
|
||||
|
||||
var effectiveInputState: ChatTextInputState {
|
||||
public var effectiveInputState: ChatTextInputState {
|
||||
if let editMessage = self.editMessage {
|
||||
return editMessage.inputState
|
||||
} else {
|
||||
@ -282,7 +287,7 @@ public final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equata
|
||||
self.inputLanguage = nil
|
||||
}
|
||||
|
||||
init(timestamp: Int32, composeInputState: ChatTextInputState, composeDisableUrlPreview: String?, replyMessageId: MessageId?, forwardMessageIds: [MessageId]?, editMessage: ChatEditMessageState?, selectionState: ChatInterfaceSelectionState?, messageActionsState: ChatInterfaceMessageActionsState, historyScrollState: ChatInterfaceHistoryScrollState?, mediaRecordingMode: ChatTextInputMediaRecordingButtonMode, silentPosting: Bool, inputLanguage: String?) {
|
||||
public init(timestamp: Int32, composeInputState: ChatTextInputState, composeDisableUrlPreview: String?, replyMessageId: MessageId?, forwardMessageIds: [MessageId]?, editMessage: ChatEditMessageState?, selectionState: ChatInterfaceSelectionState?, messageActionsState: ChatInterfaceMessageActionsState, historyScrollState: ChatInterfaceHistoryScrollState?, mediaRecordingMode: ChatTextInputMediaRecordingButtonMode, silentPosting: Bool, inputLanguage: String?) {
|
||||
self.timestamp = timestamp
|
||||
self.composeInputState = composeInputState
|
||||
self.composeDisableUrlPreview = composeDisableUrlPreview
|
||||
@ -437,17 +442,17 @@ public final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equata
|
||||
return lhs.composeInputState == rhs.composeInputState && lhs.replyMessageId == rhs.replyMessageId && lhs.selectionState == rhs.selectionState && lhs.editMessage == rhs.editMessage
|
||||
}
|
||||
|
||||
func withUpdatedComposeInputState(_ inputState: ChatTextInputState) -> ChatInterfaceState {
|
||||
public func withUpdatedComposeInputState(_ inputState: ChatTextInputState) -> ChatInterfaceState {
|
||||
let updatedComposeInputState = inputState
|
||||
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
|
||||
}
|
||||
|
||||
func withUpdatedComposeDisableUrlPreview(_ disableUrlPreview: String?) -> ChatInterfaceState {
|
||||
public func withUpdatedComposeDisableUrlPreview(_ disableUrlPreview: String?) -> ChatInterfaceState {
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: disableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
|
||||
}
|
||||
|
||||
func withUpdatedEffectiveInputState(_ inputState: ChatTextInputState) -> ChatInterfaceState {
|
||||
public func withUpdatedEffectiveInputState(_ inputState: ChatTextInputState) -> ChatInterfaceState {
|
||||
var updatedEditMessage = self.editMessage
|
||||
var updatedComposeInputState = self.composeInputState
|
||||
if let editMessage = self.editMessage {
|
||||
@ -459,15 +464,15 @@ public final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equata
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: updatedEditMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
|
||||
}
|
||||
|
||||
func withUpdatedReplyMessageId(_ replyMessageId: MessageId?) -> ChatInterfaceState {
|
||||
public func withUpdatedReplyMessageId(_ replyMessageId: MessageId?) -> ChatInterfaceState {
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
|
||||
}
|
||||
|
||||
func withUpdatedForwardMessageIds(_ forwardMessageIds: [MessageId]?) -> ChatInterfaceState {
|
||||
public func withUpdatedForwardMessageIds(_ forwardMessageIds: [MessageId]?) -> ChatInterfaceState {
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
|
||||
}
|
||||
|
||||
func withUpdatedSelectedMessages(_ messageIds: [MessageId]) -> ChatInterfaceState {
|
||||
public func withUpdatedSelectedMessages(_ messageIds: [MessageId]) -> ChatInterfaceState {
|
||||
var selectedIds = Set<MessageId>()
|
||||
if let selectionState = self.selectionState {
|
||||
selectedIds.formUnion(selectionState.selectedIds)
|
||||
@ -478,7 +483,7 @@ public final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equata
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
|
||||
}
|
||||
|
||||
func withToggledSelectedMessages(_ messageIds: [MessageId], value: Bool) -> ChatInterfaceState {
|
||||
public func withToggledSelectedMessages(_ messageIds: [MessageId], value: Bool) -> ChatInterfaceState {
|
||||
var selectedIds = Set<MessageId>()
|
||||
if let selectionState = self.selectionState {
|
||||
selectedIds.formUnion(selectionState.selectedIds)
|
||||
@ -493,27 +498,27 @@ public final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equata
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
|
||||
}
|
||||
|
||||
func withoutSelectionState() -> ChatInterfaceState {
|
||||
public func withoutSelectionState() -> ChatInterfaceState {
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: nil, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
|
||||
}
|
||||
|
||||
func withUpdatedTimestamp(_ timestamp: Int32) -> ChatInterfaceState {
|
||||
public func withUpdatedTimestamp(_ timestamp: Int32) -> ChatInterfaceState {
|
||||
return ChatInterfaceState(timestamp: timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
|
||||
}
|
||||
|
||||
func withUpdatedEditMessage(_ editMessage: ChatEditMessageState?) -> ChatInterfaceState {
|
||||
public func withUpdatedEditMessage(_ editMessage: ChatEditMessageState?) -> ChatInterfaceState {
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
|
||||
}
|
||||
|
||||
func withUpdatedMessageActionsState(_ f: (ChatInterfaceMessageActionsState) -> ChatInterfaceMessageActionsState) -> ChatInterfaceState {
|
||||
public func withUpdatedMessageActionsState(_ f: (ChatInterfaceMessageActionsState) -> ChatInterfaceMessageActionsState) -> ChatInterfaceState {
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: f(self.messageActionsState), historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
|
||||
}
|
||||
|
||||
func withUpdatedHistoryScrollState(_ historyScrollState: ChatInterfaceHistoryScrollState?) -> ChatInterfaceState {
|
||||
public func withUpdatedHistoryScrollState(_ historyScrollState: ChatInterfaceHistoryScrollState?) -> ChatInterfaceState {
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
|
||||
}
|
||||
|
||||
func withUpdatedMediaRecordingMode(_ mediaRecordingMode: ChatTextInputMediaRecordingButtonMode) -> ChatInterfaceState {
|
||||
public func withUpdatedMediaRecordingMode(_ mediaRecordingMode: ChatTextInputMediaRecordingButtonMode) -> ChatInterfaceState {
|
||||
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import Display
|
||||
import TelegramPresentationData
|
||||
import ListSectionHeaderNode
|
||||
|
||||
public enum ChatListSearchItemHeaderType: Int32 {
|
||||
public enum ChatListSearchItemHeaderType {
|
||||
case localPeers
|
||||
case members
|
||||
case contacts
|
||||
@ -13,7 +13,6 @@ public enum ChatListSearchItemHeaderType: Int32 {
|
||||
case globalPeers
|
||||
case deviceContacts
|
||||
case recentPeers
|
||||
case messages
|
||||
case phoneNumber
|
||||
case exceptions
|
||||
case addToExceptions
|
||||
@ -22,6 +21,109 @@ public enum ChatListSearchItemHeaderType: Int32 {
|
||||
case chats
|
||||
case chatTypes
|
||||
case faq
|
||||
case messages(Int32)
|
||||
|
||||
fileprivate func title(strings: PresentationStrings) -> String {
|
||||
switch self {
|
||||
case .localPeers:
|
||||
return strings.DialogList_SearchSectionDialogs
|
||||
case .members:
|
||||
return strings.Channel_Info_Members
|
||||
case .contacts:
|
||||
return strings.Contacts_TopSection
|
||||
case .bots:
|
||||
return strings.MemberSearch_BotSection
|
||||
case .admins:
|
||||
return strings.Channel_Management_Title
|
||||
case .globalPeers:
|
||||
return strings.DialogList_SearchSectionGlobal
|
||||
case .deviceContacts:
|
||||
return strings.Contacts_NotRegisteredSection
|
||||
case .recentPeers:
|
||||
return strings.DialogList_SearchSectionRecent
|
||||
case .phoneNumber:
|
||||
return strings.Contacts_PhoneNumber
|
||||
case .exceptions:
|
||||
return strings.GroupInfo_Permissions_Exceptions
|
||||
case .addToExceptions:
|
||||
return strings.Exceptions_AddToExceptions
|
||||
case .mapAddress:
|
||||
return strings.Map_AddressOnMap
|
||||
case .nearbyVenues:
|
||||
return strings.Map_PlacesNearby
|
||||
case .chats:
|
||||
return strings.Cache_ByPeerHeader
|
||||
case .chatTypes:
|
||||
return strings.ChatList_ChatTypesSection
|
||||
case .faq:
|
||||
return strings.Settings_FrequentlyAskedQuestions
|
||||
case let .messages(count):
|
||||
return strings.ChatList_Search_Messages(count)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var id: ChatListSearchItemHeaderId {
|
||||
switch self {
|
||||
case .localPeers:
|
||||
return .localPeers
|
||||
case .members:
|
||||
return .members
|
||||
case .contacts:
|
||||
return .contacts
|
||||
case .bots:
|
||||
return .bots
|
||||
case .admins:
|
||||
return .admins
|
||||
case .globalPeers:
|
||||
return .globalPeers
|
||||
case .deviceContacts:
|
||||
return .deviceContacts
|
||||
case .recentPeers:
|
||||
return .recentPeers
|
||||
case .phoneNumber:
|
||||
return .phoneNumber
|
||||
case .exceptions:
|
||||
return .exceptions
|
||||
case .addToExceptions:
|
||||
return .addToExceptions
|
||||
case .mapAddress:
|
||||
return .mapAddress
|
||||
case .nearbyVenues:
|
||||
return .nearbyVenues
|
||||
case .chats:
|
||||
return .chats
|
||||
case .chatTypes:
|
||||
return .chatTypes
|
||||
case .faq:
|
||||
return .faq
|
||||
case .messages:
|
||||
return .messages
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum ChatListSearchItemHeaderId: Int32 {
|
||||
case localPeers
|
||||
case members
|
||||
case contacts
|
||||
case bots
|
||||
case admins
|
||||
case globalPeers
|
||||
case deviceContacts
|
||||
case recentPeers
|
||||
case phoneNumber
|
||||
case exceptions
|
||||
case addToExceptions
|
||||
case mapAddress
|
||||
case nearbyVenues
|
||||
case chats
|
||||
case chatTypes
|
||||
case faq
|
||||
case messages
|
||||
case photos
|
||||
case links
|
||||
case files
|
||||
case music
|
||||
}
|
||||
|
||||
public final class ChatListSearchItemHeader: ListViewItemHeader {
|
||||
@ -37,7 +139,7 @@ public final class ChatListSearchItemHeader: ListViewItemHeader {
|
||||
|
||||
public init(type: ChatListSearchItemHeaderType, theme: PresentationTheme, strings: PresentationStrings, actionTitle: String? = nil, action: (() -> Void)? = nil) {
|
||||
self.type = type
|
||||
self.id = Int64(self.type.rawValue)
|
||||
self.id = Int64(self.type.id.rawValue)
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.actionTitle = actionTitle
|
||||
@ -75,43 +177,7 @@ public final class ChatListSearchItemHeaderNode: ListViewItemHeaderNode {
|
||||
|
||||
super.init()
|
||||
|
||||
switch type {
|
||||
case .localPeers:
|
||||
self.sectionHeaderNode.title = strings.DialogList_SearchSectionDialogs.uppercased()
|
||||
case .members:
|
||||
self.sectionHeaderNode.title = strings.Channel_Info_Members.uppercased()
|
||||
case .contacts:
|
||||
self.sectionHeaderNode.title = strings.Contacts_TopSection.uppercased()
|
||||
case .bots:
|
||||
self.sectionHeaderNode.title = strings.MemberSearch_BotSection.uppercased()
|
||||
case .admins:
|
||||
self.sectionHeaderNode.title = strings.Channel_Management_Title.uppercased()
|
||||
case .globalPeers:
|
||||
self.sectionHeaderNode.title = strings.DialogList_SearchSectionGlobal.uppercased()
|
||||
case .deviceContacts:
|
||||
self.sectionHeaderNode.title = strings.Contacts_NotRegisteredSection.uppercased()
|
||||
case .messages:
|
||||
self.sectionHeaderNode.title = strings.DialogList_SearchSectionMessages.uppercased()
|
||||
case .recentPeers:
|
||||
self.sectionHeaderNode.title = strings.DialogList_SearchSectionRecent.uppercased()
|
||||
case .phoneNumber:
|
||||
self.sectionHeaderNode.title = strings.Contacts_PhoneNumber.uppercased()
|
||||
case .exceptions:
|
||||
self.sectionHeaderNode.title = strings.GroupInfo_Permissions_Exceptions.uppercased()
|
||||
case .addToExceptions:
|
||||
self.sectionHeaderNode.title = strings.Exceptions_AddToExceptions.uppercased()
|
||||
case .mapAddress:
|
||||
self.sectionHeaderNode.title = strings.Map_AddressOnMap.uppercased()
|
||||
case .nearbyVenues:
|
||||
self.sectionHeaderNode.title = strings.Map_PlacesNearby.uppercased()
|
||||
case .chats:
|
||||
self.sectionHeaderNode.title = strings.Cache_ByPeerHeader.uppercased()
|
||||
case .chatTypes:
|
||||
self.sectionHeaderNode.title = strings.ChatList_ChatTypesSection.uppercased()
|
||||
case .faq:
|
||||
self.sectionHeaderNode.title = strings.Settings_FrequentlyAskedQuestions.uppercased()
|
||||
}
|
||||
|
||||
self.sectionHeaderNode.title = type.title(strings: strings).uppercased()
|
||||
self.sectionHeaderNode.action = actionTitle
|
||||
self.sectionHeaderNode.activateAction = action
|
||||
|
||||
@ -127,43 +193,7 @@ public final class ChatListSearchItemHeaderNode: ListViewItemHeaderNode {
|
||||
self.actionTitle = actionTitle
|
||||
self.action = action
|
||||
|
||||
switch type {
|
||||
case .localPeers:
|
||||
self.sectionHeaderNode.title = strings.DialogList_SearchSectionDialogs.uppercased()
|
||||
case .members:
|
||||
self.sectionHeaderNode.title = strings.Channel_Info_Members.uppercased()
|
||||
case .contacts:
|
||||
self.sectionHeaderNode.title = strings.Contacts_TopSection.uppercased()
|
||||
case .bots:
|
||||
self.sectionHeaderNode.title = strings.MemberSearch_BotSection.uppercased()
|
||||
case .admins:
|
||||
self.sectionHeaderNode.title = strings.Channel_Management_Title.uppercased()
|
||||
case .globalPeers:
|
||||
self.sectionHeaderNode.title = strings.DialogList_SearchSectionGlobal.uppercased()
|
||||
case .deviceContacts:
|
||||
self.sectionHeaderNode.title = strings.Contacts_NotRegisteredSection.uppercased()
|
||||
case .messages:
|
||||
self.sectionHeaderNode.title = strings.DialogList_SearchSectionMessages.uppercased()
|
||||
case .recentPeers:
|
||||
self.sectionHeaderNode.title = strings.DialogList_SearchSectionRecent.uppercased()
|
||||
case .phoneNumber:
|
||||
self.sectionHeaderNode.title = strings.Contacts_PhoneNumber.uppercased()
|
||||
case .exceptions:
|
||||
self.sectionHeaderNode.title = strings.GroupInfo_Permissions_Exceptions.uppercased()
|
||||
case .addToExceptions:
|
||||
self.sectionHeaderNode.title = strings.Exceptions_AddToExceptions.uppercased()
|
||||
case .mapAddress:
|
||||
self.sectionHeaderNode.title = strings.Map_AddressOnMap.uppercased()
|
||||
case .nearbyVenues:
|
||||
self.sectionHeaderNode.title = strings.Map_PlacesNearby.uppercased()
|
||||
case .chats:
|
||||
self.sectionHeaderNode.title = strings.Cache_ByPeerHeader.uppercased()
|
||||
case .chatTypes:
|
||||
self.sectionHeaderNode.title = strings.ChatList_ChatTypesSection.uppercased()
|
||||
case .faq:
|
||||
self.sectionHeaderNode.title = strings.Settings_FrequentlyAskedQuestions.uppercased()
|
||||
}
|
||||
|
||||
self.sectionHeaderNode.title = type.title(strings: strings).uppercased()
|
||||
self.sectionHeaderNode.action = actionTitle
|
||||
self.sectionHeaderNode.activateAction = action
|
||||
|
||||
|
||||
@ -46,6 +46,15 @@ static_library(
|
||||
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
|
||||
"//submodules/TooltipUI:TooltipUI",
|
||||
"//submodules/ListMessageItem:ListMessageItem",
|
||||
"//submodules/ChatMessageInteractiveMediaBadge:ChatMessageInteractiveMediaBadge",
|
||||
"//submodules/MediaPlayer:UniversalMediaPlayer",
|
||||
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
|
||||
"//submodules/GalleryData:GalleryData",
|
||||
"//submodules/InstantPageUI:InstantPageUI",
|
||||
"//submodules/ListSectionHeaderNode:ListSectionHeaderNode",
|
||||
"//submodules/ChatInterfaceState:ChatInterfaceState",
|
||||
"//submodules/GridMessageSelectionNode:GridMessageSelectionNode",
|
||||
],
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||
|
||||
@ -46,6 +46,15 @@ swift_library(
|
||||
"//submodules/ItemListPeerActionItem:ItemListPeerActionItem",
|
||||
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||
"//submodules/TooltipUI:TooltipUI",
|
||||
"//submodules/ListMessageItem:ListMessageItem",
|
||||
"//submodules/ChatMessageInteractiveMediaBadge:ChatMessageInteractiveMediaBadge",
|
||||
"//submodules/MediaPlayer:UniversalMediaPlayer",
|
||||
"//submodules/GalleryData:GalleryData",
|
||||
"//submodules/InstantPageUI:InstantPageUI",
|
||||
"//submodules/ListSectionHeaderNode:ListSectionHeaderNode",
|
||||
"//submodules/ChatInterfaceState:ChatInterfaceState",
|
||||
"//submodules/ShareController:ShareController",
|
||||
"//submodules/GridMessageSelectionNode:GridMessageSelectionNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -1548,7 +1548,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
|
||||
|
||||
let isEmpty = resolvedItems.count <= 1 || displayTabsAtBottom
|
||||
|
||||
if wasEmpty != isEmpty {
|
||||
if wasEmpty != isEmpty, strongSelf.displayNavigationBar {
|
||||
strongSelf.navigationBar?.setSecondaryContentNode(isEmpty ? nil : strongSelf.tabContainerNode)
|
||||
if let parentController = strongSelf.parent as? TabBarController {
|
||||
parentController.navigationBar?.setSecondaryContentNode(isEmpty ? nil : strongSelf.tabContainerNode)
|
||||
@ -1681,20 +1681,79 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
|
||||
if let scrollToTop = strongSelf.scrollToTop {
|
||||
scrollToTop()
|
||||
}
|
||||
|
||||
if let searchContentNode = strongSelf.searchContentNode {
|
||||
strongSelf.chatListDisplayNode.activateSearch(placeholderNode: searchContentNode.placeholderNode)
|
||||
var updatedSearchOptionsImpl: ((ChatListSearchOptions?, Bool) -> Void)?
|
||||
|
||||
if let filterContainerNodeAndActivate = strongSelf.chatListDisplayNode.activateSearch(placeholderNode: searchContentNode.placeholderNode, navigationController: strongSelf.navigationController as? NavigationController, updatedSearchOptions: { options, hasDate in
|
||||
updatedSearchOptionsImpl?(options, hasDate)
|
||||
}) {
|
||||
let (filterContainerNode, activate) = filterContainerNodeAndActivate
|
||||
strongSelf.navigationBar?.setSecondaryContentNode(filterContainerNode, animated: false)
|
||||
if let parentController = strongSelf.parent as? TabBarController {
|
||||
parentController.navigationBar?.setSecondaryContentNode(filterContainerNode, animated: true)
|
||||
}
|
||||
activate()
|
||||
|
||||
var currentHasSuggestions = true
|
||||
updatedSearchOptionsImpl = { [weak self, weak filterContainerNode] options, hasSuggestions in
|
||||
guard let strongSelf = self, let strongFilterContainerNode = filterContainerNode else {
|
||||
return
|
||||
}
|
||||
if currentHasSuggestions != hasSuggestions {
|
||||
currentHasSuggestions = hasSuggestions
|
||||
|
||||
var node: ASDisplayNode?
|
||||
if let options = options, options.messageTags != nil && !hasSuggestions {
|
||||
} else {
|
||||
node = strongFilterContainerNode
|
||||
}
|
||||
|
||||
strongSelf.navigationBar?.setSecondaryContentNode(node, animated: false)
|
||||
if let parentController = strongSelf.parent as? TabBarController {
|
||||
parentController.navigationBar?.setSecondaryContentNode(node, animated: true)
|
||||
}
|
||||
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
|
||||
if let layout = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout, transition: transition)
|
||||
(strongSelf.parent as? TabBarController)?.updateLayout(transition: transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
strongSelf.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring))
|
||||
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
|
||||
strongSelf.setDisplayNavigationBar(false, transition: transition)
|
||||
|
||||
(strongSelf.parent as? TabBarController)?.updateIsTabBarHidden(true, transition: .animated(duration: 0.4, curve: .spring))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public func deactivateSearch(animated: Bool) {
|
||||
if !self.displayNavigationBar {
|
||||
self.setDisplayNavigationBar(true, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate)
|
||||
if let searchContentNode = self.searchContentNode {
|
||||
self.chatListDisplayNode.deactivateSearch(placeholderNode: searchContentNode.placeholderNode, animated: animated)
|
||||
}
|
||||
|
||||
let filtersIsEmpty: Bool
|
||||
if let (resolvedItems, displayTabsAtBottom) = tabContainerData {
|
||||
filtersIsEmpty = resolvedItems.count <= 1 || displayTabsAtBottom
|
||||
} else {
|
||||
filtersIsEmpty = true
|
||||
}
|
||||
|
||||
self.navigationBar?.setSecondaryContentNode(filtersIsEmpty ? nil : self.tabContainerNode, animated: false)
|
||||
if let parentController = self.parent as? TabBarController {
|
||||
parentController.navigationBar?.setSecondaryContentNode(filtersIsEmpty ? nil : self.tabContainerNode, animated: animated)
|
||||
}
|
||||
|
||||
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate
|
||||
self.setDisplayNavigationBar(true, transition: transition)
|
||||
|
||||
(self.parent as? TabBarController)?.updateIsTabBarHidden(false, transition: .animated(duration: 0.4, curve: .spring))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -200,7 +200,7 @@ private final class ChatListShimmerNode: ASDisplayNode {
|
||||
}, present: { _ in })
|
||||
|
||||
let items = (0 ..< 2).map { _ -> ChatListItem in
|
||||
return ChatListItem(presentationData: chatListPresentationData, context: context, peerGroupId: .root, filterData: nil, index: ChatListIndex(pinningIndex: 0, messageIndex: MessageIndex(id: MessageId(peerId: peer1.id, namespace: 0, id: 0), timestamp: timestamp1)), content: .peer(messages: [Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer1.id, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, timestamp: timestamp1, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer1, text: "Text", attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [])], peer: RenderedPeer(peer: peer1), combinedReadState: CombinedPeerReadState(states: [(Namespaces.Message.Cloud, PeerReadState.idBased(maxIncomingReadId: 0, maxOutgoingReadId: 0, maxKnownId: 0, count: 0, markedUnread: false))]), isRemovedFromTotalUnreadCount: false, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: nil, promoInfo: nil, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)
|
||||
return ChatListItem(presentationData: chatListPresentationData, context: context, peerGroupId: .root, filterData: nil, index: ChatListIndex(pinningIndex: 0, messageIndex: MessageIndex(id: MessageId(peerId: peer1.id, namespace: 0, id: 0), timestamp: timestamp1)), content: .peer(messages: [Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer1.id, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: timestamp1, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer1, text: "Text", attributes: [], media: [], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [])], peer: RenderedPeer(peer: peer1), combinedReadState: CombinedPeerReadState(states: [(Namespaces.Message.Cloud, PeerReadState.idBased(maxIncomingReadId: 0, maxOutgoingReadId: 0, maxKnownId: 0, count: 0, markedUnread: false))]), isRemovedFromTotalUnreadCount: false, presence: nil, summaryInfo: ChatListMessageTagSummaryInfo(tagSummaryCount: nil, actionsSummaryCount: nil), embeddedState: nil, inputActivities: nil, promoInfo: nil, ignoreUnreadBadge: false, displayAsMessage: false, hasFailedMessages: false), editing: false, hasActiveRevealControls: false, selected: false, header: nil, enableContextActions: false, hiddenOffset: false, interaction: interaction)
|
||||
}
|
||||
|
||||
var itemNodes: [ChatListItemNode] = []
|
||||
@ -1147,12 +1147,12 @@ final class ChatListControllerNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
func activateSearch(placeholderNode: SearchBarPlaceholderNode) {
|
||||
func activateSearch(placeholderNode: SearchBarPlaceholderNode, navigationController: NavigationController?, updatedSearchOptions: ((ChatListSearchOptions?, Bool) -> Void)?) -> (ASDisplayNode, () -> Void)? {
|
||||
guard let (containerLayout, _, _, cleanNavigationBarHeight) = self.containerLayout, let navigationBar = self.navigationBar, self.searchDisplayController == nil else {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ChatListSearchContainerNode(context: self.context, filter: [], groupId: self.groupId, openPeer: { [weak self] peer, dismissSearch in
|
||||
let contentNode = ChatListSearchContainerNode(context: self.context, filter: [], groupId: self.groupId, openPeer: { [weak self] peer, dismissSearch in
|
||||
self?.requestOpenPeerFromSearch?(peer, dismissSearch)
|
||||
}, openDisabledPeer: { _ in
|
||||
}, openRecentPeerOptions: { [weak self] peer in
|
||||
@ -1165,25 +1165,36 @@ final class ChatListControllerNode: ASDisplayNode {
|
||||
if let requestAddContact = self?.requestAddContact {
|
||||
requestAddContact(phoneNumber)
|
||||
}
|
||||
}, peerContextAction: self.peerContextAction, present: { [weak self] c in
|
||||
self?.controller?.present(c, in: .window(.root))
|
||||
}), cancel: { [weak self] in
|
||||
}, peerContextAction: self.peerContextAction, present: { [weak self] c, a in
|
||||
self?.controller?.present(c, in: .window(.root), with: a)
|
||||
}, presentInGlobalOverlay: { [weak self] c, a in
|
||||
self?.controller?.presentInGlobalOverlay(c, with: a)
|
||||
}, navigationController: navigationController, updatedSearchOptions: { options, hasDate in
|
||||
updatedSearchOptions?(options, hasDate)
|
||||
})
|
||||
|
||||
self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: contentNode, cancel: { [weak self] in
|
||||
if let requestDeactivateSearch = self?.requestDeactivateSearch {
|
||||
requestDeactivateSearch()
|
||||
}
|
||||
})
|
||||
self.containerNode.accessibilityElementsHidden = true
|
||||
|
||||
self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: cleanNavigationBarHeight, transition: .immediate)
|
||||
self.searchDisplayController?.activate(insertSubnode: { [weak self, weak placeholderNode] subnode, isSearchBar in
|
||||
if let strongSelf = self, let strongPlaceholderNode = placeholderNode {
|
||||
if isSearchBar {
|
||||
strongPlaceholderNode.supernode?.insertSubnode(subnode, aboveSubnode: strongPlaceholderNode)
|
||||
} else {
|
||||
strongSelf.insertSubnode(subnode, belowSubnode: navigationBar)
|
||||
}
|
||||
|
||||
return (contentNode.filterContainerNode, { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
}, placeholder: placeholderNode)
|
||||
strongSelf.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: cleanNavigationBarHeight, transition: .immediate)
|
||||
strongSelf.searchDisplayController?.activate(insertSubnode: { [weak self, weak placeholderNode] subnode, isSearchBar in
|
||||
if let strongSelf = self, let strongPlaceholderNode = placeholderNode {
|
||||
if isSearchBar {
|
||||
strongPlaceholderNode.supernode?.insertSubnode(subnode, aboveSubnode: strongPlaceholderNode)
|
||||
} else {
|
||||
strongSelf.insertSubnode(subnode, belowSubnode: navigationBar)
|
||||
}
|
||||
}
|
||||
}, placeholder: placeholderNode)
|
||||
})
|
||||
}
|
||||
|
||||
func deactivateSearch(placeholderNode: SearchBarPlaceholderNode, animated: Bool) {
|
||||
|
||||
@ -625,7 +625,7 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
|
||||
let previousContentWidth = self.scrollNode.view.contentSize.width
|
||||
|
||||
if self.currentParams?.presentationData.theme !== presentationData.theme {
|
||||
self.selectedLineNode.image = generateImage(CGSize(width: 7.0, height: 4.0), rotatedContext: { size, context in
|
||||
self.selectedLineNode.image = generateImage(CGSize(width: 8.0, height: 4.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(presentationData.theme.list.itemAccentColor.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.width)))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,306 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SyncCore
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
|
||||
enum ChatListSearchFilter: Equatable {
|
||||
case media
|
||||
case links
|
||||
case files
|
||||
case music
|
||||
case voice
|
||||
case peer(PeerId, String)
|
||||
case date(Int32, String)
|
||||
|
||||
var id: Int32 {
|
||||
switch self {
|
||||
case .media:
|
||||
return 0
|
||||
case .links:
|
||||
return 1
|
||||
case .files:
|
||||
return 2
|
||||
case .music:
|
||||
return 3
|
||||
case .voice:
|
||||
return 4
|
||||
case let .peer(peerId, _):
|
||||
return peerId.id
|
||||
case let .date(date, _):
|
||||
return date
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class ItemNode: ASDisplayNode {
|
||||
private let pressed: () -> Void
|
||||
|
||||
private let iconNode: ASImageNode
|
||||
private let titleNode: ImmediateTextNode
|
||||
private let buttonNode: HighlightTrackingButtonNode
|
||||
|
||||
private var selectionFraction: CGFloat = 0.0
|
||||
private(set) var unreadCount: Int = 0
|
||||
|
||||
private var isReordering: Bool = false
|
||||
|
||||
private var theme: PresentationTheme?
|
||||
|
||||
init(pressed: @escaping () -> Void) {
|
||||
self.pressed = pressed
|
||||
|
||||
let titleInset: CGFloat = 4.0
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
self.iconNode.displayWithoutProcessing = true
|
||||
|
||||
self.titleNode = ImmediateTextNode()
|
||||
self.titleNode.displaysAsynchronously = false
|
||||
self.titleNode.insets = UIEdgeInsets(top: titleInset, left: 0.0, bottom: titleInset, right: 0.0)
|
||||
|
||||
self.buttonNode = HighlightTrackingButtonNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.iconNode)
|
||||
self.addSubnode(self.buttonNode)
|
||||
|
||||
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||
self.buttonNode.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
strongSelf.iconNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.iconNode.alpha = 0.4
|
||||
|
||||
strongSelf.titleNode.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.titleNode.alpha = 0.4
|
||||
} else {
|
||||
strongSelf.iconNode.alpha = 1.0
|
||||
strongSelf.iconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
|
||||
strongSelf.titleNode.alpha = 1.0
|
||||
strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func buttonPressed() {
|
||||
self.pressed()
|
||||
}
|
||||
|
||||
func update(type: ChatListSearchFilter, presentationData: PresentationData, transition: ContainedViewLayoutTransition) {
|
||||
let title: String
|
||||
let icon: UIImage?
|
||||
|
||||
let color = presentationData.theme.list.itemSecondaryTextColor
|
||||
switch type {
|
||||
case .media:
|
||||
title = presentationData.strings.ChatList_Search_FilterMedia
|
||||
icon = generateTintedImage(image: UIImage(bundleImageName: "Chat List/Search/Media"), color: color)
|
||||
case .links:
|
||||
title = presentationData.strings.ChatList_Search_FilterLinks
|
||||
icon = generateTintedImage(image: UIImage(bundleImageName: "Chat List/Search/Links"), color: color)
|
||||
case .files:
|
||||
title = presentationData.strings.ChatList_Search_FilterFiles
|
||||
icon = generateTintedImage(image: UIImage(bundleImageName: "Chat List/Search/Files"), color: color)
|
||||
case .music:
|
||||
title = presentationData.strings.ChatList_Search_FilterMusic
|
||||
icon = generateTintedImage(image: UIImage(bundleImageName: "Chat List/Search/Music"), color: color)
|
||||
case .voice:
|
||||
title = presentationData.strings.ChatList_Search_FilterVoice
|
||||
icon = generateTintedImage(image: UIImage(bundleImageName: "Chat List/Search/Voice"), color: color)
|
||||
case let .peer(_, displayTitle):
|
||||
title = displayTitle
|
||||
icon = generateTintedImage(image: UIImage(bundleImageName: "Chat List/Search/User"), color: color)
|
||||
case let .date(_, displayTitle):
|
||||
title = displayTitle
|
||||
icon = generateTintedImage(image: UIImage(bundleImageName: "Chat List/Search/Calendar"), color: color)
|
||||
}
|
||||
|
||||
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: color)
|
||||
|
||||
if self.theme !== presentationData.theme {
|
||||
self.theme = presentationData.theme
|
||||
self.iconNode.image = icon
|
||||
}
|
||||
}
|
||||
|
||||
func updateLayout(height: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
let iconInset: CGFloat = 22.0
|
||||
if let image = self.iconNode.image {
|
||||
self.iconNode.frame = CGRect(x: 0.0, y: floorToScreenPixels((height - image.size.height) / 2.0), width: image.size.width, height: image.size.height)
|
||||
}
|
||||
|
||||
let titleSize = self.titleNode.updateLayout(CGSize(width: 160.0, height: .greatestFiniteMagnitude))
|
||||
let titleFrame = CGRect(origin: CGPoint(x: -self.titleNode.insets.left + iconInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
|
||||
self.titleNode.frame = titleFrame
|
||||
|
||||
return titleSize.width - self.titleNode.insets.left - self.titleNode.insets.right + iconInset
|
||||
}
|
||||
|
||||
func updateArea(size: CGSize, sideInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.buttonNode.frame = CGRect(origin: CGPoint(x: -sideInset, y: 0.0), size: CGSize(width: size.width + sideInset * 2.0, height: size.height))
|
||||
|
||||
self.hitTestSlop = UIEdgeInsets(top: 0.0, left: -sideInset, bottom: 0.0, right: -sideInset)
|
||||
}
|
||||
}
|
||||
|
||||
enum ChatListSearchFilterEntryId: Hashable {
|
||||
case filter(Int32)
|
||||
}
|
||||
|
||||
enum ChatListSearchFilterEntry: Equatable {
|
||||
case filter(ChatListSearchFilter)
|
||||
|
||||
var id: ChatListSearchFilterEntryId {
|
||||
switch self {
|
||||
case let .filter(filter):
|
||||
return .filter(filter.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class ChatListSearchFiltersContainerNode: ASDisplayNode {
|
||||
private let scrollNode: ASScrollNode
|
||||
private var itemNodes: [ChatListSearchFilterEntryId: ItemNode] = [:]
|
||||
|
||||
var filterPressed: ((ChatListSearchFilter) -> Void)?
|
||||
|
||||
private var currentParams: (size: CGSize, sideInset: CGFloat, filters: [ChatListSearchFilterEntry], presentationData: PresentationData)?
|
||||
|
||||
override init() {
|
||||
self.scrollNode = ASScrollNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.scrollNode.view.showsHorizontalScrollIndicator = false
|
||||
self.scrollNode.view.scrollsToTop = false
|
||||
self.scrollNode.view.delaysContentTouches = false
|
||||
self.scrollNode.view.canCancelContentTouches = true
|
||||
if #available(iOS 11.0, *) {
|
||||
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
|
||||
self.addSubnode(self.scrollNode)
|
||||
}
|
||||
|
||||
func cancelAnimations() {
|
||||
self.scrollNode.layer.removeAllAnimations()
|
||||
}
|
||||
|
||||
func update(size: CGSize, sideInset: CGFloat, filters: [ChatListSearchFilterEntry], presentationData: PresentationData, transition proposedTransition: ContainedViewLayoutTransition) {
|
||||
let isFirstTime = self.currentParams == nil
|
||||
let transition: ContainedViewLayoutTransition = isFirstTime ? .immediate : proposedTransition
|
||||
|
||||
if self.currentParams?.presentationData.theme !== presentationData.theme {
|
||||
self.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor
|
||||
}
|
||||
|
||||
self.currentParams = (size: size, sideInset: sideInset, filters: filters, presentationData: presentationData)
|
||||
|
||||
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
for i in 0 ..< filters.count {
|
||||
let filter = filters[i]
|
||||
if case let .filter(type) = filter {
|
||||
let itemNode: ItemNode
|
||||
var itemNodeTransition = transition
|
||||
if let current = self.itemNodes[filter.id] {
|
||||
itemNode = current
|
||||
} else {
|
||||
itemNodeTransition = .immediate
|
||||
itemNode = ItemNode(pressed: { [weak self] in
|
||||
self?.filterPressed?(type)
|
||||
})
|
||||
self.itemNodes[filter.id] = itemNode
|
||||
}
|
||||
itemNode.update(type: type, presentationData: presentationData, transition: itemNodeTransition)
|
||||
}
|
||||
}
|
||||
var removeKeys: [ChatListSearchFilterEntryId] = []
|
||||
for (id, _) in self.itemNodes {
|
||||
if !filters.contains(where: { $0.id == id }) {
|
||||
removeKeys.append(id)
|
||||
}
|
||||
}
|
||||
for id in removeKeys {
|
||||
if let itemNode = self.itemNodes.removeValue(forKey: id) {
|
||||
transition.updateAlpha(node: itemNode, alpha: 0.0, completion: { [weak itemNode] _ in
|
||||
itemNode?.removeFromSupernode()
|
||||
})
|
||||
transition.updateTransformScale(node: itemNode, scale: 0.1)
|
||||
}
|
||||
}
|
||||
|
||||
var tabSizes: [(ChatListSearchFilterEntryId, CGSize, ItemNode, Bool)] = []
|
||||
var totalRawTabSize: CGFloat = 0.0
|
||||
|
||||
for filter in filters {
|
||||
guard let itemNode = self.itemNodes[filter.id] else {
|
||||
continue
|
||||
}
|
||||
let wasAdded = itemNode.supernode == nil
|
||||
var itemNodeTransition = transition
|
||||
if wasAdded {
|
||||
itemNodeTransition = .immediate
|
||||
self.scrollNode.addSubnode(itemNode)
|
||||
}
|
||||
let paneNodeWidth = itemNode.updateLayout(height: size.height, transition: itemNodeTransition)
|
||||
let paneNodeSize = CGSize(width: paneNodeWidth, height: size.height)
|
||||
tabSizes.append((filter.id, paneNodeSize, itemNode, wasAdded))
|
||||
totalRawTabSize += paneNodeSize.width
|
||||
}
|
||||
|
||||
let minSpacing: CGFloat = 24.0
|
||||
var spacing = minSpacing
|
||||
|
||||
let resolvedSideInset: CGFloat = 16.0 + sideInset
|
||||
var leftOffset: CGFloat = resolvedSideInset
|
||||
|
||||
var longTitlesWidth: CGFloat = resolvedSideInset
|
||||
var titlesWidth: CGFloat = 0.0
|
||||
for i in 0 ..< tabSizes.count {
|
||||
let (_, paneNodeSize, _, _) = tabSizes[i]
|
||||
longTitlesWidth += paneNodeSize.width
|
||||
titlesWidth += paneNodeSize.width
|
||||
if i != tabSizes.count - 1 {
|
||||
longTitlesWidth += minSpacing
|
||||
}
|
||||
}
|
||||
longTitlesWidth += resolvedSideInset
|
||||
|
||||
let verticalOffset: CGFloat = -3.0
|
||||
for i in 0 ..< tabSizes.count {
|
||||
let (_, paneNodeSize, paneNode, wasAdded) = tabSizes[i]
|
||||
let itemNodeTransition = transition
|
||||
|
||||
let paneFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - paneNodeSize.height) / 2.0) + verticalOffset), size: paneNodeSize)
|
||||
|
||||
if wasAdded {
|
||||
paneNode.frame = paneFrame
|
||||
paneNode.alpha = 0.0
|
||||
paneNode.subnodeTransform = CATransform3DMakeScale(0.1, 0.1, 1.0)
|
||||
itemNodeTransition.updateSublayerTransformScale(node: paneNode, scale: 1.0)
|
||||
itemNodeTransition.updateAlpha(node: paneNode, alpha: 1.0)
|
||||
} else {
|
||||
itemNodeTransition.updateFrameAdditive(node: paneNode, frame: paneFrame)
|
||||
}
|
||||
|
||||
paneNode.updateArea(size: paneFrame.size, sideInset: spacing / 2.0, transition: itemNodeTransition)
|
||||
paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -spacing / 2.0, bottom: 0.0, right: -spacing / 2.0)
|
||||
|
||||
leftOffset += paneNodeSize.width + spacing
|
||||
}
|
||||
leftOffset -= spacing
|
||||
leftOffset += resolvedSideInset
|
||||
|
||||
self.scrollNode.view.contentSize = CGSize(width: leftOffset, height: size.height)
|
||||
}
|
||||
}
|
||||
1135
submodules/ChatListUI/Sources/ChatListSearchMediaNode.swift
Normal file
1135
submodules/ChatListUI/Sources/ChatListSearchMediaNode.swift
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,182 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import AppBundle
|
||||
|
||||
final class ChatListSearchMessageSelectionPanelNode: ASDisplayNode {
|
||||
private let context: AccountContext
|
||||
private var theme: PresentationTheme
|
||||
|
||||
private let deleteMessages: () -> Void
|
||||
private let shareMessages: () -> Void
|
||||
private let forwardMessages: () -> Void
|
||||
|
||||
private let separatorNode: ASDisplayNode
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let deleteButton: HighlightableButtonNode
|
||||
private let forwardButton: HighlightableButtonNode
|
||||
private let shareButton: HighlightableButtonNode
|
||||
|
||||
private var actions: ChatAvailableMessageActions?
|
||||
|
||||
private let canDeleteMessagesDisposable = MetaDisposable()
|
||||
|
||||
private var validLayout: ContainerViewLayout?
|
||||
|
||||
var selectedMessages = Set<MessageId>() {
|
||||
didSet {
|
||||
if oldValue != self.selectedMessages {
|
||||
self.forwardButton.isEnabled = self.selectedMessages.count != 0
|
||||
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
if self.selectedMessages.isEmpty {
|
||||
self.actions = nil
|
||||
if let layout = self.validLayout {
|
||||
let _ = self.update(layout: layout, presentationData: presentationData, transition: .immediate)
|
||||
}
|
||||
self.canDeleteMessagesDisposable.set(nil)
|
||||
} else {
|
||||
self.canDeleteMessagesDisposable.set((self.context.sharedContext.chatAvailableMessageActions(postbox: context.account.postbox, accountPeerId: context.account.peerId, messageIds: self.selectedMessages)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] actions in
|
||||
if let strongSelf = self {
|
||||
strongSelf.actions = actions
|
||||
if let layout = strongSelf.validLayout {
|
||||
let _ = strongSelf.update(layout: layout, presentationData: presentationData, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(context: AccountContext, deleteMessages: @escaping () -> Void, shareMessages: @escaping () -> Void, forwardMessages: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.deleteMessages = deleteMessages
|
||||
self.shareMessages = shareMessages
|
||||
self.forwardMessages = forwardMessages
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.theme = presentationData.theme
|
||||
|
||||
self.separatorNode = ASDisplayNode()
|
||||
self.separatorNode.backgroundColor = presentationData.theme.chat.inputPanel.panelSeparatorColor
|
||||
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.backgroundColor = presentationData.theme.chat.inputPanel.panelBackgroundColor
|
||||
|
||||
self.deleteButton = HighlightableButtonNode(pointerStyle: .default)
|
||||
self.deleteButton.isEnabled = false
|
||||
self.deleteButton.isAccessibilityElement = true
|
||||
self.deleteButton.accessibilityLabel = presentationData.strings.VoiceOver_MessageContextDelete
|
||||
|
||||
self.forwardButton = HighlightableButtonNode(pointerStyle: .default)
|
||||
self.forwardButton.isAccessibilityElement = true
|
||||
self.forwardButton.accessibilityLabel = presentationData.strings.VoiceOver_MessageContextForward
|
||||
|
||||
self.shareButton = HighlightableButtonNode(pointerStyle: .default)
|
||||
self.shareButton.isEnabled = false
|
||||
self.shareButton.isAccessibilityElement = true
|
||||
self.shareButton.accessibilityLabel = presentationData.strings.VoiceOver_MessageContextShare
|
||||
|
||||
self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: presentationData.theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
|
||||
self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: presentationData.theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled])
|
||||
self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: presentationData.theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
|
||||
self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: presentationData.theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled])
|
||||
self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: presentationData.theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
|
||||
self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: presentationData.theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled])
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.deleteButton)
|
||||
self.addSubnode(self.forwardButton)
|
||||
self.addSubnode(self.shareButton)
|
||||
self.addSubnode(self.separatorNode)
|
||||
|
||||
self.forwardButton.isEnabled = false
|
||||
|
||||
self.deleteButton.addTarget(self, action: #selector(self.deleteButtonPressed), forControlEvents: .touchUpInside)
|
||||
self.forwardButton.addTarget(self, action: #selector(self.forwardButtonPressed), forControlEvents: .touchUpInside)
|
||||
self.shareButton.addTarget(self, action: #selector(self.shareButtonPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.canDeleteMessagesDisposable.dispose()
|
||||
}
|
||||
|
||||
func update(layout: ContainerViewLayout, presentationData: PresentationData, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
if presentationData.theme !== self.theme {
|
||||
self.theme = presentationData.theme
|
||||
|
||||
self.backgroundNode.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor
|
||||
self.separatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor
|
||||
|
||||
self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: presentationData.theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
|
||||
self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: presentationData.theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled])
|
||||
self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: presentationData.theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
|
||||
self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: presentationData.theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled])
|
||||
self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: presentationData.theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
|
||||
self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: presentationData.theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled])
|
||||
}
|
||||
|
||||
let width = layout.size.width
|
||||
let insets = layout.insets(options: [])
|
||||
let leftInset = insets.left
|
||||
let rightInset = insets.left
|
||||
|
||||
let panelHeight: CGFloat
|
||||
if case .regular = layout.metrics.widthClass, case .regular = layout.metrics.heightClass {
|
||||
panelHeight = 49.0
|
||||
} else {
|
||||
panelHeight = 45.0
|
||||
}
|
||||
|
||||
if let actions = self.actions {
|
||||
self.deleteButton.isEnabled = false
|
||||
self.forwardButton.isEnabled = actions.options.contains(.forward)
|
||||
self.shareButton.isEnabled = false
|
||||
|
||||
self.deleteButton.isEnabled = !actions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty
|
||||
self.shareButton.isEnabled = !actions.options.intersection([.forward]).isEmpty
|
||||
|
||||
self.deleteButton.isHidden = !self.deleteButton.isEnabled
|
||||
} else {
|
||||
self.deleteButton.isEnabled = false
|
||||
self.deleteButton.isHidden = true
|
||||
self.forwardButton.isEnabled = false
|
||||
self.shareButton.isEnabled = false
|
||||
}
|
||||
|
||||
self.deleteButton.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: 57.0, height: panelHeight))
|
||||
self.forwardButton.frame = CGRect(origin: CGPoint(x: width - rightInset - 57.0, y: 0.0), size: CGSize(width: 57.0, height: panelHeight))
|
||||
self.shareButton.frame = CGRect(origin: CGPoint(x: floor((width - rightInset - 57.0) / 2.0), y: 0.0), size: CGSize(width: 57.0, height: panelHeight))
|
||||
|
||||
let panelHeightWithInset = panelHeight + layout.intrinsicInsets.bottom - 49.0
|
||||
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: panelHeightWithInset)))
|
||||
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: UIScreenPixel), size: CGSize(width: layout.size.width, height: UIScreenPixel)))
|
||||
|
||||
return panelHeightWithInset
|
||||
}
|
||||
|
||||
@objc func deleteButtonPressed() {
|
||||
self.deleteMessages()
|
||||
}
|
||||
|
||||
@objc func forwardButtonPressed() {
|
||||
self.forwardMessages()
|
||||
}
|
||||
|
||||
@objc func shareButtonPressed() {
|
||||
self.shareMessages()
|
||||
}
|
||||
}
|
||||
144
submodules/ChatListUI/Sources/DateSuggestion.swift
Normal file
144
submodules/ChatListUI/Sources/DateSuggestion.swift
Normal file
@ -0,0 +1,144 @@
|
||||
import Foundation
|
||||
import TelegramPresentationData
|
||||
|
||||
private let telegramReleaseDate = Date(timeIntervalSince1970: 1376438400.0)
|
||||
|
||||
func suggestDates(for string: String, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat) -> [(Date, String?)] {
|
||||
let string = string.folding(options: .diacriticInsensitive, locale: .current).trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
if string.count < 3 {
|
||||
return []
|
||||
}
|
||||
|
||||
let months: [Int: (String, String)] = [
|
||||
1: (strings.Month_GenJanuary, strings.Month_ShortJanuary),
|
||||
2: (strings.Month_GenFebruary, strings.Month_ShortFebruary),
|
||||
3: (strings.Month_GenMarch, strings.Month_ShortMarch),
|
||||
4: (strings.Month_GenApril, strings.Month_ShortApril),
|
||||
5: (strings.Month_GenMay, strings.Month_ShortMay),
|
||||
6: (strings.Month_GenJune, strings.Month_ShortJune),
|
||||
7: (strings.Month_GenJuly, strings.Month_ShortJuly),
|
||||
8: (strings.Month_GenAugust, strings.Month_ShortAugust),
|
||||
9: (strings.Month_GenSeptember, strings.Month_ShortSeptember),
|
||||
10: (strings.Month_GenOctober, strings.Month_ShortOctober),
|
||||
11: (strings.Month_GenNovember, strings.Month_ShortNovember),
|
||||
12: (strings.Month_GenDecember, strings.Month_ShortDecember)
|
||||
]
|
||||
|
||||
let weekDays: [Int: (String, String)] = [
|
||||
1: (strings.Weekday_Monday, strings.Weekday_ShortMonday),
|
||||
2: (strings.Weekday_Tuesday, strings.Weekday_ShortTuesday),
|
||||
3: (strings.Weekday_Wednesday, strings.Weekday_ShortWednesday),
|
||||
4: (strings.Weekday_Thursday, strings.Weekday_ShortThursday),
|
||||
5: (strings.Weekday_Friday, strings.Weekday_ShortFriday),
|
||||
6: (strings.Weekday_Saturday, strings.Weekday_ShortSaturday),
|
||||
7: (strings.Weekday_Sunday, strings.Weekday_ShortSunday.lowercased()),
|
||||
]
|
||||
|
||||
let today = strings.Weekday_Today
|
||||
let yesterday = strings.Weekday_Yesterday
|
||||
let dateSeparator = dateTimeFormat.dateSeparator
|
||||
|
||||
var result: [(Date, String?)] = []
|
||||
|
||||
let calendar = Calendar.current
|
||||
func getUpperDate(for date: Date) -> Date {
|
||||
let components = calendar.dateComponents(in: .current, from: date)
|
||||
let upperComponents = DateComponents(year: components.year, month: components.month, day: components.day, hour: 23, minute: 59, second: 59)
|
||||
return calendar.date(from: upperComponents)!
|
||||
}
|
||||
|
||||
let now = Date()
|
||||
let nowComponents = calendar.dateComponents(in: .current, from: now)
|
||||
guard let year = nowComponents.year else {
|
||||
return []
|
||||
}
|
||||
|
||||
let midnight = calendar.startOfDay(for: now)
|
||||
if today.lowercased().hasPrefix(string) {
|
||||
let todayDate = getUpperDate(for: midnight)
|
||||
result.append((todayDate, today))
|
||||
}
|
||||
if yesterday.lowercased().hasPrefix(string) {
|
||||
let yesterdayMidnight = calendar.date(byAdding: .day, value: -1, to: midnight)!
|
||||
let yesterdayDate = getUpperDate(for: yesterdayMidnight)
|
||||
result.append((yesterdayDate, yesterday))
|
||||
}
|
||||
|
||||
func getUpperMonthDate(month: Int, year: Int) -> Date {
|
||||
let monthComponents = DateComponents(year: year, month: month)
|
||||
let date = calendar.date(from: monthComponents)!
|
||||
let range = calendar.range(of: .day, in: .month, for: date)!
|
||||
let numDays = range.count
|
||||
let upperComponents = DateComponents(year: year, month: month, day: numDays, hour: 23, minute: 59, second: 59)
|
||||
return calendar.date(from: upperComponents)!
|
||||
}
|
||||
|
||||
let decimalRange = string.rangeOfCharacter(from: .decimalDigits)
|
||||
if decimalRange != nil {
|
||||
if string.count == 4, let value = Int(string), value <= year {
|
||||
let date = getUpperMonthDate(month: 12, year: value)
|
||||
if date > telegramReleaseDate {
|
||||
result.append((date, "\(value)"))
|
||||
}
|
||||
} else if !dateSeparator.isEmpty && string.contains(dateSeparator) {
|
||||
let stringComponents = string.components(separatedBy: dateSeparator)
|
||||
if stringComponents.count > 1 {
|
||||
let locale = Locale(identifier: strings.baseLanguageCode)
|
||||
do {
|
||||
let dd = try NSDataDetector(types: NSTextCheckingResult.CheckingType.date.rawValue)
|
||||
if let match = dd.firstMatch(in: string, options: [], range: NSMakeRange(0, string.utf16.count)), let date = match.date, date > telegramReleaseDate {
|
||||
var resultDate = date
|
||||
if resultDate > now {
|
||||
if let date = calendar.date(byAdding: .year, value: -1, to: resultDate) {
|
||||
resultDate = date
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0..<5 {
|
||||
if let date = calendar.date(byAdding: .year, value: -i, to: resultDate) {
|
||||
result.append((date, nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (day, value) in weekDays {
|
||||
let dayName = value.0.lowercased()
|
||||
let shortDayName = value.1.lowercased()
|
||||
if string == shortDayName || (string.count >= shortDayName.count && dayName.hasPrefix(string)) {
|
||||
var nextDateComponent = calendar.dateComponents([.hour, .minute, .second], from: now)
|
||||
nextDateComponent.weekday = day + calendar.firstWeekday
|
||||
if let date = calendar.nextDate(after: now, matching: nextDateComponent, matchingPolicy: .nextTime, direction: .backward) {
|
||||
let upperDate = getUpperDate(for: date)
|
||||
for i in 0..<5 {
|
||||
if let date = calendar.date(byAdding: .hour, value: -24 * 7 * i, to: upperDate) {
|
||||
if calendar.isDate(date, equalTo: now, toGranularity: .weekOfYear) {
|
||||
result.append((date, value.0))
|
||||
} else {
|
||||
result.append((date, nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (month, value) in months {
|
||||
let monthName = value.0.lowercased()
|
||||
let shortMonthName = value.1.lowercased()
|
||||
if string == shortMonthName || (string.count >= shortMonthName.count && monthName.hasPrefix(string)) {
|
||||
for i in (year - 7 ... year).reversed() {
|
||||
let date = getUpperMonthDate(month: month, year: i)
|
||||
if date <= now && date > telegramReleaseDate {
|
||||
result.append((date, nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@ -637,7 +637,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
|
||||
if let peer = peer {
|
||||
var overrideImage: AvatarNodeImageOverride?
|
||||
if peer.id == item.context.account.peerId && !displayAsMessage {
|
||||
if peer.id.isReplies {
|
||||
overrideImage = .repliesIcon
|
||||
} else if peer.id == item.context.account.peerId && !displayAsMessage {
|
||||
overrideImage = .savedMessagesIcon
|
||||
} else if peer.isDeleted {
|
||||
overrideImage = .deletedIcon
|
||||
@ -1099,7 +1101,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
titleAttributedString = NSAttributedString(string: item.presentationData.strings.ChatList_ArchivedChatsTitle, font: titleFont, textColor: theme.titleColor)
|
||||
} else if itemPeer.chatMainPeer?.id == item.context.account.peerId {
|
||||
titleAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_SavedMessages, font: titleFont, textColor: theme.titleColor)
|
||||
} else if let displayTitle = itemPeer.chatMainPeer?.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) {
|
||||
} else if let id = itemPeer.chatMainPeer?.id, id.isReplies {
|
||||
titleAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_Replies, font: titleFont, textColor: theme.titleColor)
|
||||
} else if let displayTitle = itemPeer.chatMainPeer?.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) {
|
||||
titleAttributedString = NSAttributedString(string: displayTitle, font: titleFont, textColor: item.index.messageIndex.id.peerId.namespace == Namespaces.Peer.SecretChat ? theme.secretTitleColor : theme.titleColor)
|
||||
}
|
||||
case .group:
|
||||
|
||||
20
submodules/ChatMessageInteractiveMediaBadge/BUCK
Normal file
20
submodules/ChatMessageInteractiveMediaBadge/BUCK
Normal file
@ -0,0 +1,20 @@
|
||||
load("//Config:buck_rule_macros.bzl", "static_library")
|
||||
|
||||
static_library(
|
||||
name = "ChatMessageInteractiveMediaBadge",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit#shared",
|
||||
"//submodules/Display:Display#shared",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
"//submodules/RadialStatusNode:RadialStatusNode",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
],
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||
"$SDKROOT/System/Library/Frameworks/UIKit.framework",
|
||||
],
|
||||
)
|
||||
20
submodules/ChatMessageInteractiveMediaBadge/BUILD
Normal file
20
submodules/ChatMessageInteractiveMediaBadge/BUILD
Normal file
@ -0,0 +1,20 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ChatMessageInteractiveMediaBadge",
|
||||
module_name = "ChatMessageInteractiveMediaBadge",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
"//submodules/RadialStatusNode:RadialStatusNode",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
@ -7,18 +7,21 @@ import TextFormat
|
||||
import RadialStatusNode
|
||||
import AppBundle
|
||||
|
||||
enum ChatMessageInteractiveMediaDownloadState: Equatable {
|
||||
private let font = Font.regular(11.0)
|
||||
private let boldFont = Font.semibold(11.0)
|
||||
|
||||
public enum ChatMessageInteractiveMediaDownloadState: Equatable {
|
||||
case remote
|
||||
case fetching(progress: Float?)
|
||||
case compactRemote
|
||||
case compactFetching(progress: Float)
|
||||
}
|
||||
|
||||
enum ChatMessageInteractiveMediaBadgeContent: Equatable {
|
||||
public enum ChatMessageInteractiveMediaBadgeContent: Equatable {
|
||||
case text(inset: CGFloat, backgroundColor: UIColor, foregroundColor: UIColor, text: NSAttributedString)
|
||||
case mediaDownload(backgroundColor: UIColor, foregroundColor: UIColor, duration: String, size: String?, muted: Bool, active: Bool)
|
||||
|
||||
static func ==(lhs: ChatMessageInteractiveMediaBadgeContent, rhs: ChatMessageInteractiveMediaBadgeContent) -> Bool {
|
||||
public static func ==(lhs: ChatMessageInteractiveMediaBadgeContent, rhs: ChatMessageInteractiveMediaBadgeContent) -> Bool {
|
||||
switch lhs {
|
||||
case let .text(lhsInset, lhsBackgroundColor, lhsForegroundColor, lhsText):
|
||||
if case let .text(rhsInset, rhsBackgroundColor, rhsForegroundColor, rhsText) = rhs, lhsInset.isEqual(to: rhsInset), lhsBackgroundColor.isEqual(rhsBackgroundColor), lhsForegroundColor.isEqual(rhsForegroundColor), lhsText.isEqual(to: rhsText) {
|
||||
@ -36,12 +39,9 @@ enum ChatMessageInteractiveMediaBadgeContent: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
private let font = Font.regular(11.0)
|
||||
private let boldFont = Font.semibold(11.0)
|
||||
|
||||
final class ChatMessageInteractiveMediaBadge: ASDisplayNode {
|
||||
public final class ChatMessageInteractiveMediaBadge: ASDisplayNode {
|
||||
private var content: ChatMessageInteractiveMediaBadgeContent?
|
||||
var pressed: (() -> Void)?
|
||||
public var pressed: (() -> Void)?
|
||||
|
||||
private var mediaDownloadState: ChatMessageInteractiveMediaDownloadState?
|
||||
|
||||
@ -56,7 +56,7 @@ final class ChatMessageInteractiveMediaBadge: ASDisplayNode {
|
||||
private var iconNode: ASImageNode?
|
||||
private var mediaDownloadStatusNode: RadialStatusNode?
|
||||
|
||||
override init() {
|
||||
override public init() {
|
||||
self.backgroundNode = ASImageNode()
|
||||
self.backgroundNode.clipsToBounds = true
|
||||
self.durationNode = ASTextNode()
|
||||
@ -68,7 +68,7 @@ final class ChatMessageInteractiveMediaBadge: ASDisplayNode {
|
||||
self.backgroundNode.addSubnode(self.durationNode)
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
override public func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
@ -87,7 +87,7 @@ final class ChatMessageInteractiveMediaBadge: ASDisplayNode {
|
||||
return self.measureNode.measure(CGSize(width: 240.0, height: 160.0)).width
|
||||
}
|
||||
|
||||
func update(theme: PresentationTheme?, content: ChatMessageInteractiveMediaBadgeContent?, mediaDownloadState: ChatMessageInteractiveMediaDownloadState?, alignment: NSTextAlignment = .left, animated: Bool, badgeAnimated: Bool = true) {
|
||||
public func update(theme: PresentationTheme?, content: ChatMessageInteractiveMediaBadgeContent?, mediaDownloadState: ChatMessageInteractiveMediaDownloadState?, alignment: NSTextAlignment = .left, animated: Bool, badgeAnimated: Bool = true) {
|
||||
var transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate
|
||||
|
||||
let previousContentSize = self.previousContentSize
|
||||
@ -297,7 +297,7 @@ final class ChatMessageInteractiveMediaBadge: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
override public func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
return self.backgroundNode.frame.contains(point)
|
||||
}
|
||||
}
|
||||
@ -140,6 +140,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
|
||||
let deletePeer: ((PeerId) -> Void)?
|
||||
let itemHighlighting: ContactItemHighlighting?
|
||||
let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
|
||||
let arrowAction: (() -> Void)?
|
||||
|
||||
public let selectable: Bool
|
||||
|
||||
@ -147,7 +148,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
|
||||
|
||||
public let header: ListViewItemHeader?
|
||||
|
||||
public init(presentationData: ItemListPresentationData, style: ItemListStyle = .plain, sectionId: ItemListSectionId = 0, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, context: AccountContext, peerMode: ContactsPeerItemPeerMode, peer: ContactsPeerItemPeer, status: ContactsPeerItemStatus, badge: ContactsPeerItemBadge? = nil, enabled: Bool, selection: ContactsPeerItemSelection, editing: ContactsPeerItemEditing, options: [ItemListPeerItemRevealOption] = [], additionalActions: [ContactsPeerItemAction] = [], actionIcon: ContactsPeerItemActionIcon = .none, index: PeerNameIndex?, header: ListViewItemHeader?, action: @escaping (ContactsPeerItemPeer) -> Void, disabledAction: ((ContactsPeerItemPeer) -> Void)? = nil, setPeerIdWithRevealedOptions: ((PeerId?, PeerId?) -> Void)? = nil, deletePeer: ((PeerId) -> Void)? = nil, itemHighlighting: ContactItemHighlighting? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil) {
|
||||
public init(presentationData: ItemListPresentationData, style: ItemListStyle = .plain, sectionId: ItemListSectionId = 0, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, context: AccountContext, peerMode: ContactsPeerItemPeerMode, peer: ContactsPeerItemPeer, status: ContactsPeerItemStatus, badge: ContactsPeerItemBadge? = nil, enabled: Bool, selection: ContactsPeerItemSelection, editing: ContactsPeerItemEditing, options: [ItemListPeerItemRevealOption] = [], additionalActions: [ContactsPeerItemAction] = [], actionIcon: ContactsPeerItemActionIcon = .none, index: PeerNameIndex?, header: ListViewItemHeader?, action: @escaping (ContactsPeerItemPeer) -> Void, disabledAction: ((ContactsPeerItemPeer) -> Void)? = nil, setPeerIdWithRevealedOptions: ((PeerId?, PeerId?) -> Void)? = nil, deletePeer: ((PeerId) -> Void)? = nil, itemHighlighting: ContactItemHighlighting? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil, arrowAction: (() -> Void)? = nil) {
|
||||
self.presentationData = presentationData
|
||||
self.style = style
|
||||
self.sectionId = sectionId
|
||||
@ -172,6 +173,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
|
||||
self.itemHighlighting = itemHighlighting
|
||||
self.selectable = enabled || disabledAction != nil
|
||||
self.contextAction = contextAction
|
||||
self.arrowAction = arrowAction
|
||||
|
||||
if let index = index {
|
||||
var letter: String = "#"
|
||||
@ -318,6 +320,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
private var badgeTextNode: TextNode?
|
||||
private var selectionNode: CheckNode?
|
||||
private var actionButtonNodes: [HighlightableButtonNode]?
|
||||
private var arrowButtonNode: HighlightableButtonNode?
|
||||
|
||||
private var isHighlighted: Bool = false
|
||||
|
||||
@ -503,6 +506,11 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
break
|
||||
}
|
||||
|
||||
var arrowButtonImage: UIImage?
|
||||
if let _ = item.arrowAction {
|
||||
arrowButtonImage = generateTintedImage(image: UIImage(bundleImageName: "Chat List/Search/Arrow"), color: item.presentationData.theme.list.disclosureArrowColor)
|
||||
}
|
||||
|
||||
var actionButtons: [ActionButton]?
|
||||
struct ActionButton {
|
||||
let image: UIImage?
|
||||
@ -548,6 +556,8 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
if let user = peer as? TelegramUser {
|
||||
if peer.id == item.context.account.peerId, case .generalSearch = item.peerMode {
|
||||
titleAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_SavedMessages, font: titleBoldFont, textColor: textColor)
|
||||
} else if peer.id.isReplies {
|
||||
titleAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_Replies, font: titleBoldFont, textColor: textColor)
|
||||
} else if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty {
|
||||
let string = NSMutableAttributedString()
|
||||
switch item.displayOrder {
|
||||
@ -665,6 +675,10 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
|
||||
additionalTitleInset += badgeSize
|
||||
|
||||
if let arrowButtonImage = arrowButtonImage {
|
||||
additionalTitleInset += arrowButtonImage.size.width + 4.0
|
||||
}
|
||||
|
||||
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: 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()))
|
||||
@ -735,6 +749,8 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
var overrideImage: AvatarNodeImageOverride?
|
||||
if peer.id == item.context.account.peerId, case .generalSearch = item.peerMode {
|
||||
overrideImage = .savedMessagesIcon
|
||||
} else if peer.id.isReplies, case .generalSearch = item.peerMode {
|
||||
overrideImage = .repliesIcon
|
||||
} else if peer.isDeleted {
|
||||
overrideImage = .deletedIcon
|
||||
}
|
||||
@ -856,6 +872,23 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
actionButtonNodes.forEach { $0.removeFromSupernode() }
|
||||
}
|
||||
|
||||
if let arrowButtonImage = arrowButtonImage {
|
||||
if strongSelf.arrowButtonNode == nil {
|
||||
let arrowButtonNode = HighlightableButtonNode()
|
||||
arrowButtonNode.addTarget(self, action: #selector(strongSelf.arrowButtonPressed), forControlEvents: .touchUpInside)
|
||||
strongSelf.arrowButtonNode = arrowButtonNode
|
||||
strongSelf.containerNode.addSubnode(arrowButtonNode)
|
||||
}
|
||||
if let arrowButtonNode = strongSelf.arrowButtonNode {
|
||||
arrowButtonNode.setImage(arrowButtonImage, for: .normal)
|
||||
|
||||
transition.updateFrame(node: arrowButtonNode, frame: CGRect(origin: CGPoint(x: params.width - params.rightInset - 12.0 - arrowButtonImage.size.width, y: floor((nodeLayout.contentSize.height - arrowButtonImage.size.height) / 2.0)), size: arrowButtonImage.size))
|
||||
}
|
||||
} else if let arrowButtonNode = strongSelf.arrowButtonNode {
|
||||
strongSelf.arrowButtonNode = nil
|
||||
arrowButtonNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
let badgeBackgroundWidth: CGFloat
|
||||
if let currentBadgeBackgroundImage = currentBadgeBackgroundImage, let (badgeTextLayout, badgeTextApply) = badgeTextLayoutAndApply {
|
||||
let badgeBackgroundNode: ASImageNode
|
||||
@ -876,7 +909,12 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
badgeBackgroundNode.image = currentBadgeBackgroundImage
|
||||
|
||||
badgeBackgroundWidth = max(badgeTextLayout.size.width + 10.0, currentBadgeBackgroundImage.size.width)
|
||||
let badgeBackgroundFrame = CGRect(x: revealOffset + params.width - params.rightInset - badgeBackgroundWidth - 6.0, y: floor((nodeLayout.contentSize.height - currentBadgeBackgroundImage.size.height) / 2.0), width: badgeBackgroundWidth, height: currentBadgeBackgroundImage.size.height)
|
||||
var badgeBackgroundFrame = CGRect(x: revealOffset + params.width - params.rightInset - badgeBackgroundWidth - 6.0, y: floor((nodeLayout.contentSize.height - currentBadgeBackgroundImage.size.height) / 2.0), width: badgeBackgroundWidth, height: currentBadgeBackgroundImage.size.height)
|
||||
|
||||
if let arrowButtonImage = arrowButtonImage {
|
||||
badgeBackgroundFrame.origin.x -= arrowButtonImage.size.width + 6.0
|
||||
}
|
||||
|
||||
let badgeTextFrame = CGRect(origin: CGPoint(x: badgeBackgroundFrame.midX - badgeTextLayout.size.width / 2.0, y: badgeBackgroundFrame.minY + 2.0), size: badgeTextLayout.size)
|
||||
|
||||
let badgeTextNode = badgeTextApply()
|
||||
@ -1064,4 +1102,10 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@objc func arrowButtonPressed() {
|
||||
if let (item, _, _, _, _, _) = self.layoutParams {
|
||||
item.arrowAction?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,6 +77,8 @@ private final class DeleteChatPeerActionSheetItemNode: ActionSheetItemNode {
|
||||
|
||||
if chatPeer.id == context.account.peerId {
|
||||
self.avatarNode.setPeer(context: context, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: peer, overrideImage: .savedMessagesIcon)
|
||||
} else if chatPeer.id.isReplies {
|
||||
self.avatarNode.setPeer(context: context, theme: (context.sharedContext.currentPresentationData.with { $0 }).theme, peer: peer, overrideImage: .repliesIcon)
|
||||
} else {
|
||||
var overrideImage: AvatarNodeImageOverride?
|
||||
if chatPeer.isDeleted {
|
||||
|
||||
@ -3,6 +3,17 @@ import AsyncDisplayKit
|
||||
|
||||
private var backArrowImageCache: [Int32: UIImage] = [:]
|
||||
|
||||
class SparseNode: ASDisplayNode {
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let result = super.hitTest(point, with: event)
|
||||
if result != self.view {
|
||||
return result
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class NavigationBarTheme {
|
||||
public static func generateBackArrowImage(color: UIColor) -> UIImage? {
|
||||
return generateImage(CGSize(width: 13.0, height: 22.0), rotatedContext: { size, context in
|
||||
@ -126,7 +137,8 @@ open class NavigationBar: ASDisplayNode {
|
||||
}
|
||||
|
||||
private let stripeNode: ASDisplayNode
|
||||
private let clippingNode: ASDisplayNode
|
||||
private let clippingNode: SparseNode
|
||||
private let buttonsContainerNode: ASDisplayNode
|
||||
|
||||
public private(set) var contentNode: NavigationBarContentNode?
|
||||
public private(set) var secondaryContentNode: ASDisplayNode?
|
||||
@ -260,7 +272,7 @@ open class NavigationBar: ASDisplayNode {
|
||||
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.bold(17.0), textColor: self.presentationData.theme.primaryTextColor)
|
||||
self.titleNode.accessibilityLabel = title
|
||||
if self.titleNode.supernode == nil {
|
||||
self.clippingNode.addSubnode(self.titleNode)
|
||||
self.buttonsContainerNode.addSubnode(self.titleNode)
|
||||
}
|
||||
} else {
|
||||
self.titleNode.removeFromSupernode()
|
||||
@ -279,7 +291,7 @@ open class NavigationBar: ASDisplayNode {
|
||||
}
|
||||
|
||||
if let titleView = self.titleView {
|
||||
self.clippingNode.view.addSubview(titleView)
|
||||
self.buttonsContainerNode.view.addSubview(titleView)
|
||||
}
|
||||
|
||||
self.invalidateCalculatedLayout()
|
||||
@ -499,7 +511,7 @@ open class NavigationBar: ASDisplayNode {
|
||||
}
|
||||
|
||||
if self.leftButtonNode.supernode == nil {
|
||||
self.clippingNode.addSubnode(self.leftButtonNode)
|
||||
self.buttonsContainerNode.addSubnode(self.leftButtonNode)
|
||||
}
|
||||
|
||||
if animated {
|
||||
@ -540,9 +552,9 @@ open class NavigationBar: ASDisplayNode {
|
||||
if let backTitle = backTitle {
|
||||
self.backButtonNode.updateManualText(backTitle)
|
||||
if self.backButtonNode.supernode == nil {
|
||||
self.clippingNode.addSubnode(self.backButtonNode)
|
||||
self.clippingNode.addSubnode(self.backButtonArrow)
|
||||
self.clippingNode.addSubnode(self.badgeNode)
|
||||
self.buttonsContainerNode.addSubnode(self.backButtonNode)
|
||||
self.buttonsContainerNode.addSubnode(self.backButtonArrow)
|
||||
self.buttonsContainerNode.addSubnode(self.badgeNode)
|
||||
}
|
||||
|
||||
if animated {
|
||||
@ -588,7 +600,7 @@ open class NavigationBar: ASDisplayNode {
|
||||
}
|
||||
self.rightButtonNode.updateItems(items)
|
||||
if self.rightButtonNode.supernode == nil {
|
||||
self.clippingNode.addSubnode(self.rightButtonNode)
|
||||
self.buttonsContainerNode.addSubnode(self.rightButtonNode)
|
||||
}
|
||||
if animated {
|
||||
self.rightButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
@ -648,25 +660,25 @@ open class NavigationBar: ASDisplayNode {
|
||||
if let transitionTitleNode = value.navigationBar?.makeTransitionTitleNode(foregroundColor: self.presentationData.theme.primaryTextColor) {
|
||||
self.transitionTitleNode = transitionTitleNode
|
||||
if self.leftButtonNode.supernode != nil {
|
||||
self.clippingNode.insertSubnode(transitionTitleNode, belowSubnode: self.leftButtonNode)
|
||||
self.buttonsContainerNode.insertSubnode(transitionTitleNode, belowSubnode: self.leftButtonNode)
|
||||
} else if self.backButtonNode.supernode != nil {
|
||||
self.clippingNode.insertSubnode(transitionTitleNode, belowSubnode: self.backButtonNode)
|
||||
self.buttonsContainerNode.insertSubnode(transitionTitleNode, belowSubnode: self.backButtonNode)
|
||||
} else {
|
||||
self.clippingNode.addSubnode(transitionTitleNode)
|
||||
self.buttonsContainerNode.addSubnode(transitionTitleNode)
|
||||
}
|
||||
}
|
||||
case .bottom:
|
||||
if let transitionBackButtonNode = value.navigationBar?.makeTransitionBackButtonNode(accentColor: self.presentationData.theme.buttonColor) {
|
||||
self.transitionBackButtonNode = transitionBackButtonNode
|
||||
self.clippingNode.addSubnode(transitionBackButtonNode)
|
||||
self.buttonsContainerNode.addSubnode(transitionBackButtonNode)
|
||||
}
|
||||
if let transitionBackArrowNode = value.navigationBar?.makeTransitionBackArrowNode(accentColor: self.presentationData.theme.buttonColor) {
|
||||
self.transitionBackArrowNode = transitionBackArrowNode
|
||||
self.clippingNode.addSubnode(transitionBackArrowNode)
|
||||
self.buttonsContainerNode.addSubnode(transitionBackArrowNode)
|
||||
}
|
||||
if let transitionBadgeNode = value.navigationBar?.makeTransitionBadgeNode() {
|
||||
self.transitionBadgeNode = transitionBadgeNode
|
||||
self.clippingNode.addSubnode(transitionBadgeNode)
|
||||
self.buttonsContainerNode.addSubnode(transitionBadgeNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -701,9 +713,12 @@ open class NavigationBar: ASDisplayNode {
|
||||
self.rightButtonNode = NavigationButtonNode()
|
||||
self.rightButtonNode.hitTestSlop = UIEdgeInsets(top: -4.0, left: -4.0, bottom: -4.0, right: -10.0)
|
||||
|
||||
self.clippingNode = ASDisplayNode()
|
||||
self.clippingNode = SparseNode()
|
||||
self.clippingNode.clipsToBounds = true
|
||||
|
||||
self.buttonsContainerNode = ASDisplayNode()
|
||||
self.buttonsContainerNode.clipsToBounds = true
|
||||
|
||||
self.backButtonNode.color = self.presentationData.theme.buttonColor
|
||||
self.backButtonNode.disabledColor = self.presentationData.theme.disabledButtonColor
|
||||
self.leftButtonNode.color = self.presentationData.theme.buttonColor
|
||||
@ -720,6 +735,7 @@ open class NavigationBar: ASDisplayNode {
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.buttonsContainerNode)
|
||||
self.addSubnode(self.clippingNode)
|
||||
|
||||
self.backgroundColor = self.presentationData.theme.backgroundColor
|
||||
@ -821,7 +837,7 @@ open class NavigationBar: ASDisplayNode {
|
||||
self.validLayout = (size, defaultHeight, additionalHeight, leftInset, rightInset, appearsHidden)
|
||||
|
||||
if let secondaryContentNode = self.secondaryContentNode {
|
||||
transition.updateAlpha(node: secondaryContentNode, alpha: appearsHidden ? 0.0 : 1.0)
|
||||
// transition.updateAlpha(node: secondaryContentNode, alpha: appearsHidden ? 0.0 : 1.0)
|
||||
}
|
||||
|
||||
let apparentAdditionalHeight: CGFloat = self.secondaryContentNode != nil ? NavigationBar.defaultSecondaryContentHeight : 0.0
|
||||
@ -830,6 +846,7 @@ open class NavigationBar: ASDisplayNode {
|
||||
let backButtonInset: CGFloat = leftInset + 27.0
|
||||
|
||||
transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||
transition.updateFrame(node: self.buttonsContainerNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||
var expansionHeight: CGFloat = 0.0
|
||||
if let contentNode = self.contentNode {
|
||||
var contentNodeFrame: CGRect
|
||||
@ -839,7 +856,9 @@ open class NavigationBar: ASDisplayNode {
|
||||
contentNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))
|
||||
case .expansion:
|
||||
expansionHeight = contentNode.height
|
||||
contentNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - expansionHeight - apparentAdditionalHeight), size: CGSize(width: size.width, height: expansionHeight))
|
||||
|
||||
let additionalExpansionHeight: CGFloat = self.secondaryContentNode != nil && appearsHidden ? NavigationBar.defaultSecondaryContentHeight : 0.0
|
||||
contentNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - expansionHeight - apparentAdditionalHeight - additionalExpansionHeight), size: CGSize(width: size.width, height: expansionHeight))
|
||||
if appearsHidden {
|
||||
if self.secondaryContentNode != nil {
|
||||
contentNodeFrame.origin.y += NavigationBar.defaultSecondaryContentHeight
|
||||
@ -1174,10 +1193,10 @@ open class NavigationBar: ASDisplayNode {
|
||||
contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
if case .replacement = contentNode.mode, !self.clippingNode.alpha.isZero {
|
||||
self.clippingNode.alpha = 0.0
|
||||
if case .replacement = contentNode.mode, !self.buttonsContainerNode.alpha.isZero {
|
||||
self.buttonsContainerNode.alpha = 0.0
|
||||
if animated {
|
||||
self.clippingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
self.buttonsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1187,23 +1206,36 @@ open class NavigationBar: ASDisplayNode {
|
||||
} else {
|
||||
self.requestLayout()
|
||||
}
|
||||
} else if self.clippingNode.alpha.isZero {
|
||||
self.clippingNode.alpha = 1.0
|
||||
} else if self.buttonsContainerNode.alpha.isZero {
|
||||
self.buttonsContainerNode.alpha = 1.0
|
||||
if animated {
|
||||
self.clippingNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.buttonsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func setSecondaryContentNode(_ secondatryContentNode: ASDisplayNode?) {
|
||||
if self.secondaryContentNode !== secondatryContentNode {
|
||||
public func setSecondaryContentNode(_ secondaryContentNode: ASDisplayNode?, animated: Bool = false) {
|
||||
if self.secondaryContentNode !== secondaryContentNode {
|
||||
if let previous = self.secondaryContentNode {
|
||||
previous.removeFromSupernode()
|
||||
if animated && previous.supernode === self.clippingNode {
|
||||
previous.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak previous] finished in
|
||||
if finished {
|
||||
previous?.removeFromSupernode()
|
||||
previous?.layer.removeAllAnimations()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
previous.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
self.secondaryContentNode = secondatryContentNode
|
||||
if let secondaryContentNode = secondatryContentNode {
|
||||
self.secondaryContentNode = secondaryContentNode
|
||||
if let secondaryContentNode = secondaryContentNode {
|
||||
self.clippingNode.addSubnode(secondaryContentNode)
|
||||
|
||||
if animated {
|
||||
secondaryContentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1223,11 +1255,11 @@ open class NavigationBar: ASDisplayNode {
|
||||
if let contentNode = self.contentNode, case .replacement = contentNode.mode {
|
||||
} else {
|
||||
let targetAlpha: CGFloat = hidden ? 0.0 : 1.0
|
||||
let previousAlpha = self.clippingNode.alpha
|
||||
let previousAlpha = self.buttonsContainerNode.alpha
|
||||
if previousAlpha != targetAlpha {
|
||||
self.clippingNode.alpha = targetAlpha
|
||||
self.buttonsContainerNode.alpha = targetAlpha
|
||||
if animated {
|
||||
self.clippingNode.layer.animateAlpha(from: previousAlpha, to: targetAlpha, duration: 0.2)
|
||||
self.buttonsContainerNode.layer.animateAlpha(from: previousAlpha, to: targetAlpha, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1247,7 +1279,7 @@ open class NavigationBar: ASDisplayNode {
|
||||
return nil
|
||||
}
|
||||
|
||||
if result == self.view || result == self.clippingNode.view {
|
||||
if result == self.view || result == self.buttonsContainerNode.view {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -340,9 +340,9 @@ open class TabBarController: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
public func updateLayout() {
|
||||
public func updateLayout(transition: ContainedViewLayoutTransition = .immediate) {
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, transition: .immediate)
|
||||
self.containerLayoutUpdated(layout, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -400,6 +400,9 @@ public enum TabBarItemContextActionType {
|
||||
if let contentNode = navigationBar.contentNode, case .expansion = contentNode.mode, !self.displayNavigationBar {
|
||||
navigationBarFrame.origin.y += contentNode.height + statusBarHeight
|
||||
}
|
||||
if let _ = navigationBar.contentNode, let _ = navigationBar.secondaryContentNode, !self.displayNavigationBar {
|
||||
navigationBarFrame.origin.y += NavigationBar.defaultSecondaryContentHeight
|
||||
}
|
||||
navigationBar.updateLayout(size: navigationBarFrame.size, defaultHeight: defaultNavigationBarHeight, additionalHeight: 0.0, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, appearsHidden: !self.displayNavigationBar, transition: transition)
|
||||
if !transition.isAnimated {
|
||||
navigationBar.layer.cancelAnimationsRecursive(key: "bounds")
|
||||
|
||||
23
submodules/FileMediaResourceStatus/BUCK
Normal file
23
submodules/FileMediaResourceStatus/BUCK
Normal file
@ -0,0 +1,23 @@
|
||||
load("//Config:buck_rule_macros.bzl", "static_library")
|
||||
|
||||
static_library(
|
||||
name = "FileMediaResourceStatus",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit#shared",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit#shared",
|
||||
"//submodules/Display:Display#shared",
|
||||
"//submodules/Postbox:Postbox#shared",
|
||||
"//submodules/TelegramCore:TelegramCore#shared",
|
||||
"//submodules/SyncCore:SyncCore#shared",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/MediaPlayer:UniversalMediaPlayer",
|
||||
],
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||
"$SDKROOT/System/Library/Frameworks/UIKit.framework",
|
||||
"$SDKROOT/System/Library/Frameworks/WebKit.framework",
|
||||
],
|
||||
)
|
||||
22
submodules/FileMediaResourceStatus/BUILD
Normal file
22
submodules/FileMediaResourceStatus/BUILD
Normal file
@ -0,0 +1,22 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "FileMediaResourceStatus",
|
||||
module_name = "FileMediaResourceStatus",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/SyncCore:SyncCore",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/MediaPlayer:UniversalMediaPlayer",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
@ -7,12 +7,12 @@ import SwiftSignalKit
|
||||
import UniversalMediaPlayer
|
||||
import AccountContext
|
||||
|
||||
private func internalMessageFileMediaPlaybackStatus(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool) -> Signal<MediaPlayerStatus?, NoError> {
|
||||
private func internalMessageFileMediaPlaybackStatus(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool, isGlobalSearch: Bool) -> Signal<MediaPlayerStatus?, NoError> {
|
||||
guard let playerType = peerMessageMediaPlayerType(message) else {
|
||||
return .single(nil)
|
||||
}
|
||||
|
||||
if let (playlistId, itemId) = peerMessagesMediaPlaylistAndItemId(message, isRecentActions: isRecentActions) {
|
||||
if let (playlistId, itemId) = peerMessagesMediaPlaylistAndItemId(message, isRecentActions: isRecentActions, isGlobalSearch: isGlobalSearch) {
|
||||
return context.sharedContext.mediaManager.filteredPlaylistState(accountId: context.account.id, playlistId: playlistId, itemId: itemId, type: playerType)
|
||||
|> mapToSignal { state -> Signal<MediaPlayerStatus?, NoError> in
|
||||
return .single(state?.status)
|
||||
@ -22,31 +22,31 @@ private func internalMessageFileMediaPlaybackStatus(context: AccountContext, fil
|
||||
}
|
||||
}
|
||||
|
||||
func messageFileMediaPlaybackStatus(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool) -> Signal<MediaPlayerStatus, NoError> {
|
||||
public func messageFileMediaPlaybackStatus(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool, isGlobalSearch: Bool) -> Signal<MediaPlayerStatus, NoError> {
|
||||
var duration = 0.0
|
||||
if let value = file.duration {
|
||||
duration = Double(value)
|
||||
}
|
||||
let defaultStatus = MediaPlayerStatus(generationTimestamp: 0.0, duration: duration, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused, soundEnabled: true)
|
||||
return internalMessageFileMediaPlaybackStatus(context: context, file: file, message: message, isRecentActions: isRecentActions) |> map { status in
|
||||
return internalMessageFileMediaPlaybackStatus(context: context, file: file, message: message, isRecentActions: isRecentActions, isGlobalSearch: isGlobalSearch) |> map { status in
|
||||
return status ?? defaultStatus
|
||||
}
|
||||
}
|
||||
|
||||
func messageFileMediaPlaybackAudioLevelEvents(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool) -> Signal<Float, NoError> {
|
||||
public func messageFileMediaPlaybackAudioLevelEvents(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool, isGlobalSearch: Bool) -> Signal<Float, NoError> {
|
||||
guard let playerType = peerMessageMediaPlayerType(message) else {
|
||||
return .never()
|
||||
}
|
||||
|
||||
if let (playlistId, itemId) = peerMessagesMediaPlaylistAndItemId(message, isRecentActions: isRecentActions) {
|
||||
if let (playlistId, itemId) = peerMessagesMediaPlaylistAndItemId(message, isRecentActions: isRecentActions, isGlobalSearch: isGlobalSearch) {
|
||||
return context.sharedContext.mediaManager.filteredPlayerAudioLevelEvents(accountId: context.account.id, playlistId: playlistId, itemId: itemId, type: playerType)
|
||||
} else {
|
||||
return .never()
|
||||
}
|
||||
}
|
||||
|
||||
func messageFileMediaResourceStatus(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool, isSharedMedia: Bool = false) -> Signal<FileMediaResourceStatus, NoError> {
|
||||
let playbackStatus = internalMessageFileMediaPlaybackStatus(context: context, file: file, message: message, isRecentActions: isRecentActions) |> map { status -> MediaPlayerPlaybackStatus? in
|
||||
public func messageFileMediaResourceStatus(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool, isSharedMedia: Bool = false, isGlobalSearch: Bool = false) -> Signal<FileMediaResourceStatus, NoError> {
|
||||
let playbackStatus = internalMessageFileMediaPlaybackStatus(context: context, file: file, message: message, isRecentActions: isRecentActions, isGlobalSearch: isGlobalSearch) |> map { status -> MediaPlayerPlaybackStatus? in
|
||||
return status?.status
|
||||
}
|
||||
|
||||
31
submodules/GalleryData/BUCK
Normal file
31
submodules/GalleryData/BUCK
Normal file
@ -0,0 +1,31 @@
|
||||
load("//Config:buck_rule_macros.bzl", "static_library")
|
||||
|
||||
static_library(
|
||||
name = "GalleryData",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit#shared",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit#shared",
|
||||
"//submodules/Display:Display#shared",
|
||||
"//submodules/Postbox:Postbox#shared",
|
||||
"//submodules/TelegramCore:TelegramCore#shared",
|
||||
"//submodules/SyncCore:SyncCore#shared",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||
"//submodules/InstantPageUI:InstantPageUI",
|
||||
"//submodules/GalleryUI:GalleryUI",
|
||||
"//submodules/PeerAvatarGalleryUI:PeerAvatarGalleryUI",
|
||||
"//submodules/MediaResources:MediaResources",
|
||||
"//submodules/WebsiteType:WebsiteType",
|
||||
],
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||
"$SDKROOT/System/Library/Frameworks/UIKit.framework",
|
||||
"$SDKROOT/System/Library/Frameworks/QuickLook.framework",
|
||||
"$SDKROOT/System/Library/Frameworks/SafariServices.framework",
|
||||
],
|
||||
)
|
||||
29
submodules/GalleryData/BUILD
Normal file
29
submodules/GalleryData/BUILD
Normal file
@ -0,0 +1,29 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "GalleryData",
|
||||
module_name = "GalleryData",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/SyncCore:SyncCore",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||
"//submodules/InstantPageUI:InstantPageUI",
|
||||
"//submodules/GalleryUI:GalleryUI",
|
||||
"//submodules/PeerAvatarGalleryUI:PeerAvatarGalleryUI",
|
||||
"//submodules/MediaResources:MediaResources",
|
||||
"//submodules/WebsiteType:WebsiteType",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
296
submodules/GalleryData/Sources/GalleryData.swift
Normal file
296
submodules/GalleryData/Sources/GalleryData.swift
Normal file
@ -0,0 +1,296 @@
|
||||
import Foundation
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
import SwiftSignalKit
|
||||
import PassKit
|
||||
import Lottie
|
||||
import TelegramUIPreferences
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import InstantPageUI
|
||||
import PeerAvatarGalleryUI
|
||||
import GalleryUI
|
||||
import MediaResources
|
||||
import WebsiteType
|
||||
|
||||
public enum ChatMessageGalleryControllerData {
|
||||
case url(String)
|
||||
case pass(TelegramMediaFile)
|
||||
case instantPage(InstantPageGalleryController, Int, Media)
|
||||
case map(TelegramMediaMap)
|
||||
case stickerPack(StickerPackReference)
|
||||
case audio(TelegramMediaFile)
|
||||
case document(TelegramMediaFile, Bool)
|
||||
case gallery(Signal<GalleryController, NoError>)
|
||||
case secretGallery(SecretMediaPreviewController)
|
||||
case chatAvatars(AvatarGalleryController, Media)
|
||||
case theme(TelegramMediaFile)
|
||||
case other(Media)
|
||||
}
|
||||
|
||||
private func instantPageBlockMedia(pageId: MediaId, block: InstantPageBlock, media: [MediaId: Media], counter: inout Int) -> [InstantPageGalleryEntry] {
|
||||
switch block {
|
||||
case let .image(id, caption, _, _):
|
||||
if let m = media[id] {
|
||||
let result = [InstantPageGalleryEntry(index: Int32(counter), pageId: pageId, media: InstantPageMedia(index: counter, media: m, url: nil, caption: caption.text, credit: caption.credit), caption: caption.text, credit: caption.credit, location: InstantPageGalleryEntryLocation(position: Int32(counter), totalCount: 0))]
|
||||
counter += 1
|
||||
return result
|
||||
}
|
||||
case let .video(id, caption, _, _):
|
||||
if let m = media[id] {
|
||||
let result = [InstantPageGalleryEntry(index: Int32(counter), pageId: pageId, media: InstantPageMedia(index: counter, media: m, url: nil, caption: caption.text, credit: caption.credit), caption: caption.text, credit: caption.credit, location: InstantPageGalleryEntryLocation(position: Int32(counter), totalCount: 0))]
|
||||
counter += 1
|
||||
return result
|
||||
}
|
||||
case let .collage(items, _):
|
||||
var result: [InstantPageGalleryEntry] = []
|
||||
for item in items {
|
||||
result.append(contentsOf: instantPageBlockMedia(pageId: pageId, block: item, media: media, counter: &counter))
|
||||
}
|
||||
return result
|
||||
case let .slideshow(items, _):
|
||||
var result: [InstantPageGalleryEntry] = []
|
||||
for item in items {
|
||||
result.append(contentsOf: instantPageBlockMedia(pageId: pageId, block: item, media: media, counter: &counter))
|
||||
}
|
||||
return result
|
||||
default:
|
||||
break
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
public func instantPageGalleryMedia(webpageId: MediaId, page: InstantPage, galleryMedia: Media) -> [InstantPageGalleryEntry] {
|
||||
var result: [InstantPageGalleryEntry] = []
|
||||
var counter: Int = 0
|
||||
|
||||
for block in page.blocks {
|
||||
result.append(contentsOf: instantPageBlockMedia(pageId: webpageId, block: block, media: page.media, counter: &counter))
|
||||
}
|
||||
|
||||
var found = false
|
||||
for item in result {
|
||||
if item.media.media.id == galleryMedia.id {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
result.insert(InstantPageGalleryEntry(index: Int32(counter), pageId: webpageId, media: InstantPageMedia(index: counter, media: galleryMedia, url: nil, caption: nil, credit: nil), caption: nil, credit: nil, location: InstantPageGalleryEntryLocation(position: Int32(counter), totalCount: 0)), at: 0)
|
||||
}
|
||||
|
||||
for i in 0 ..< result.count {
|
||||
let item = result[i]
|
||||
result[i] = InstantPageGalleryEntry(index: Int32(i), pageId: item.pageId, media: item.media, caption: item.caption, credit: item.credit, location: InstantPageGalleryEntryLocation(position: Int32(i), totalCount: Int32(result.count)))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func chatMessageGalleryControllerData(context: AccountContext, chatLocation: ChatLocation?, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>?, message: Message, navigationController: NavigationController?, standalone: Bool, reverseMessageGalleryOrder: Bool, mode: ChatControllerInteractionOpenMessageMode, source: GalleryControllerItemSource?, synchronousLoad: Bool, actionInteraction: GalleryControllerActionInteraction?) -> ChatMessageGalleryControllerData? {
|
||||
var galleryMedia: Media?
|
||||
var otherMedia: Media?
|
||||
var instantPageMedia: (TelegramMediaWebpage, [InstantPageGalleryEntry])?
|
||||
for media in message.media {
|
||||
if let action = media as? TelegramMediaAction {
|
||||
switch action.action {
|
||||
case let .photoUpdated(image):
|
||||
if let peer = messageMainPeer(message), let image = image {
|
||||
let promise: Promise<[AvatarGalleryEntry]> = Promise([AvatarGalleryEntry.image(image.imageId, image.reference, image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(message), media: media), resource: $0.resource)) }), image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(message), media: media), resource: $0.resource)) }), peer, message.timestamp, nil, message.id, image.immediateThumbnailData, "action")])
|
||||
let galleryController = AvatarGalleryController(context: context, peer: peer, sourceCorners: .roundRect(15.5), remoteEntries: promise, skipInitial: true, replaceRootController: { controller, ready in
|
||||
|
||||
})
|
||||
return .chatAvatars(galleryController, image)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
} else if let file = media as? TelegramMediaFile {
|
||||
galleryMedia = file
|
||||
} else if let image = media as? TelegramMediaImage {
|
||||
galleryMedia = image
|
||||
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
|
||||
if let file = content.file {
|
||||
galleryMedia = file
|
||||
} else if let image = content.image {
|
||||
if case .link = mode {
|
||||
} else if ["photo", "document", "video", "gif", "telegram_album"].contains(content.type) {
|
||||
galleryMedia = image
|
||||
}
|
||||
}
|
||||
|
||||
if let instantPage = content.instantPage, let galleryMedia = galleryMedia {
|
||||
switch instantPageType(of: content) {
|
||||
case .album:
|
||||
let medias = instantPageGalleryMedia(webpageId: webpage.webpageId, page: instantPage, galleryMedia: galleryMedia)
|
||||
if medias.count > 1 {
|
||||
instantPageMedia = (webpage, medias)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if let mapMedia = media as? TelegramMediaMap {
|
||||
galleryMedia = mapMedia
|
||||
} else if let contactMedia = media as? TelegramMediaContact {
|
||||
otherMedia = contactMedia
|
||||
}
|
||||
}
|
||||
|
||||
var stream = false
|
||||
var autoplayingVideo = false
|
||||
var landscape = false
|
||||
var timecode: Double? = nil
|
||||
|
||||
switch mode {
|
||||
case .stream:
|
||||
stream = true
|
||||
case .automaticPlayback:
|
||||
autoplayingVideo = true
|
||||
case .landscape:
|
||||
autoplayingVideo = true
|
||||
landscape = true
|
||||
case let .timecode(time):
|
||||
timecode = time
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if let (webPage, instantPageMedia) = instantPageMedia, let galleryMedia = galleryMedia {
|
||||
var centralIndex: Int = 0
|
||||
for i in 0 ..< instantPageMedia.count {
|
||||
if instantPageMedia[i].media.media.id == galleryMedia.id {
|
||||
centralIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let gallery = InstantPageGalleryController(context: context, webPage: webPage, message: message, entries: instantPageMedia, centralIndex: centralIndex, fromPlayingVideo: autoplayingVideo, landscape: landscape, timecode: timecode, replaceRootController: { [weak navigationController] controller, ready in
|
||||
if let navigationController = navigationController {
|
||||
navigationController.replaceTopController(controller, animated: false, ready: ready)
|
||||
}
|
||||
}, baseNavigationController: navigationController)
|
||||
return .instantPage(gallery, centralIndex, galleryMedia)
|
||||
} else if let galleryMedia = galleryMedia {
|
||||
if let mapMedia = galleryMedia as? TelegramMediaMap {
|
||||
return .map(mapMedia)
|
||||
} else if let file = galleryMedia as? TelegramMediaFile, (file.isSticker || file.isAnimatedSticker) {
|
||||
for attribute in file.attributes {
|
||||
if case let .Sticker(_, reference, _) = attribute {
|
||||
if let reference = reference {
|
||||
return .stickerPack(reference)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if let file = galleryMedia as? TelegramMediaFile, file.isAnimatedSticker {
|
||||
return nil
|
||||
} else if let file = galleryMedia as? TelegramMediaFile, file.isMusic || file.isVoice || file.isInstantVideo {
|
||||
return .audio(file)
|
||||
} else if let file = galleryMedia as? TelegramMediaFile, file.mimeType == "application/vnd.apple.pkpass" || (file.fileName != nil && file.fileName!.lowercased().hasSuffix(".pkpass")) {
|
||||
return .pass(file)
|
||||
} else {
|
||||
if let file = galleryMedia as? TelegramMediaFile {
|
||||
if let fileName = file.fileName {
|
||||
let ext = (fileName as NSString).pathExtension.lowercased()
|
||||
if ext == "tgios-theme" {
|
||||
return .theme(file)
|
||||
} else if ext == "wav" || ext == "opus" {
|
||||
return .audio(file)
|
||||
} else if ext == "json", let fileSize = file.size, fileSize < 1024 * 1024 {
|
||||
if let path = context.account.postbox.mediaBox.completedResourcePath(file.resource), let composition = LOTComposition(filePath: path), composition.timeDuration > 0.0 {
|
||||
let gallery = GalleryController(context: context, source: .peerMessagesAtId(messageId: message.id, chatLocation: chatLocation ?? ChatLocation.peer(message.id.peerId), chatLocationContextHolder: chatLocationContextHolder ?? Atomic<ChatLocationContextHolder?>(value: nil)), invertItemOrder: reverseMessageGalleryOrder, streamSingleVideo: stream, fromPlayingVideo: autoplayingVideo, landscape: landscape, timecode: timecode, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in
|
||||
navigationController?.replaceTopController(controller, animated: false, ready: ready)
|
||||
}, baseNavigationController: navigationController, actionInteraction: actionInteraction)
|
||||
return .gallery(.single(gallery))
|
||||
}
|
||||
}
|
||||
|
||||
if ext == "mkv" {
|
||||
return .document(file, true)
|
||||
}
|
||||
}
|
||||
|
||||
if internalDocumentItemSupportsMimeType(file.mimeType, fileName: file.fileName ?? "file") {
|
||||
let gallery = GalleryController(context: context, source: source ?? .peerMessagesAtId(messageId: message.id, chatLocation: chatLocation ?? .peer(message.id.peerId), chatLocationContextHolder: chatLocationContextHolder ?? Atomic<ChatLocationContextHolder?>(value: nil)), invertItemOrder: reverseMessageGalleryOrder, streamSingleVideo: stream, fromPlayingVideo: autoplayingVideo, landscape: landscape, timecode: timecode, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in
|
||||
navigationController?.replaceTopController(controller, animated: false, ready: ready)
|
||||
}, baseNavigationController: navigationController, actionInteraction: actionInteraction)
|
||||
return .gallery(.single(gallery))
|
||||
}
|
||||
|
||||
if !file.isVideo {
|
||||
return .document(file, false)
|
||||
}
|
||||
}
|
||||
|
||||
if message.containsSecretMedia {
|
||||
let gallery = SecretMediaPreviewController(context: context, messageId: message.id)
|
||||
return .secretGallery(gallery)
|
||||
} else {
|
||||
let startTimecode: Signal<Double?, NoError>
|
||||
if let timecode = timecode {
|
||||
startTimecode = .single(timecode)
|
||||
} else {
|
||||
startTimecode = mediaPlaybackStoredState(postbox: context.account.postbox, messageId: message.id)
|
||||
|> map { state in
|
||||
return state?.timestamp
|
||||
}
|
||||
}
|
||||
|
||||
return .gallery(startTimecode
|
||||
|> deliverOnMainQueue
|
||||
|> map { timecode in
|
||||
let gallery = GalleryController(context: context, source: source ?? (standalone ? .standaloneMessage(message) : .peerMessagesAtId(messageId: message.id, chatLocation: chatLocation ?? .peer(message.id.peerId), chatLocationContextHolder: chatLocationContextHolder ?? Atomic<ChatLocationContextHolder?>(value: nil))), invertItemOrder: reverseMessageGalleryOrder, streamSingleVideo: stream, fromPlayingVideo: autoplayingVideo, landscape: landscape, timecode: timecode, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in
|
||||
navigationController?.replaceTopController(controller, animated: false, ready: ready)
|
||||
}, baseNavigationController: navigationController, actionInteraction: actionInteraction)
|
||||
gallery.temporaryDoNotWaitForReady = autoplayingVideo
|
||||
return gallery
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if let otherMedia = otherMedia {
|
||||
return .other(otherMedia)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public enum ChatMessagePreviewControllerData {
|
||||
case instantPage(InstantPageGalleryController, Int, Media)
|
||||
case gallery(GalleryController)
|
||||
}
|
||||
|
||||
public func chatMessagePreviewControllerData(context: AccountContext, chatLocation: ChatLocation?, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>?, message: Message, standalone: Bool, reverseMessageGalleryOrder: Bool, navigationController: NavigationController?) -> ChatMessagePreviewControllerData? {
|
||||
if let mediaData = chatMessageGalleryControllerData(context: context, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, message: message, navigationController: navigationController, standalone: standalone, reverseMessageGalleryOrder: reverseMessageGalleryOrder, mode: .default, source: nil, synchronousLoad: true, actionInteraction: nil) {
|
||||
switch mediaData {
|
||||
case let .gallery(gallery):
|
||||
break
|
||||
case let .instantPage(gallery, centralIndex, galleryMedia):
|
||||
return .instantPage(gallery, centralIndex, galleryMedia)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func chatMediaListPreviewControllerData(context: AccountContext, chatLocation: ChatLocation?, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>?, message: Message, standalone: Bool, reverseMessageGalleryOrder: Bool, navigationController: NavigationController?) -> Signal<ChatMessagePreviewControllerData?, NoError> {
|
||||
if let mediaData = chatMessageGalleryControllerData(context: context, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, message: message, navigationController: navigationController, standalone: standalone, reverseMessageGalleryOrder: reverseMessageGalleryOrder, mode: .default, source: nil, synchronousLoad: true, actionInteraction: nil) {
|
||||
switch mediaData {
|
||||
case let .gallery(gallery):
|
||||
return gallery
|
||||
|> map { gallery in
|
||||
return .gallery(gallery)
|
||||
}
|
||||
case let .instantPage(gallery, centralIndex, galleryMedia):
|
||||
return .single(.instantPage(gallery, centralIndex, galleryMedia))
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return .single(nil)
|
||||
}
|
||||
@ -246,20 +246,20 @@ public final class GalleryControllerPresentationArguments {
|
||||
|
||||
private enum GalleryMessageHistoryView {
|
||||
case view(MessageHistoryView)
|
||||
case single(MessageHistoryEntry)
|
||||
case entries([MessageHistoryEntry], Bool, Bool)
|
||||
|
||||
var entries: [MessageHistoryEntry] {
|
||||
switch self {
|
||||
case let .view(view):
|
||||
return view.entries
|
||||
case let .single(entry):
|
||||
return [entry]
|
||||
case let .entries(entries, _, _):
|
||||
return entries
|
||||
}
|
||||
}
|
||||
|
||||
var tagMask: MessageTags? {
|
||||
switch self {
|
||||
case .single:
|
||||
case .entries:
|
||||
return nil
|
||||
case let .view(view):
|
||||
return view.tagMask
|
||||
@ -268,8 +268,8 @@ private enum GalleryMessageHistoryView {
|
||||
|
||||
var hasEarlier: Bool {
|
||||
switch self {
|
||||
case .single:
|
||||
return false
|
||||
case let .entries(_, hasEarlier, _):
|
||||
return hasEarlier
|
||||
case let .view(view):
|
||||
return view.earlierId != nil
|
||||
}
|
||||
@ -277,19 +277,14 @@ private enum GalleryMessageHistoryView {
|
||||
|
||||
var hasLater: Bool {
|
||||
switch self {
|
||||
case .single:
|
||||
return false
|
||||
case let .entries(_ , _, hasLater):
|
||||
return hasLater
|
||||
case let .view(view):
|
||||
return view.laterId != nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum GalleryControllerItemSource {
|
||||
case peerMessagesAtId(MessageId)
|
||||
case standaloneMessage(Message)
|
||||
}
|
||||
|
||||
public enum GalleryControllerInteractionTapAction {
|
||||
case url(url: String, concealed: Bool)
|
||||
case textMention(String)
|
||||
@ -357,6 +352,7 @@ public class GalleryController: ViewController, StandalonePresentableController
|
||||
private var entries: [MessageHistoryEntry] = []
|
||||
private var hasLeftEntries: Bool = false
|
||||
private var hasRightEntries: Bool = false
|
||||
private var loadingMore: Bool = false
|
||||
private var tagMask: MessageTags?
|
||||
private var centralEntryStableId: UInt32?
|
||||
private var configuration: GalleryConfiguration?
|
||||
@ -417,17 +413,23 @@ public class GalleryController: ViewController, StandalonePresentableController
|
||||
|
||||
let message: Signal<Message?, NoError>
|
||||
switch source {
|
||||
case let .peerMessagesAtId(messageId):
|
||||
case let .peerMessagesAtId(messageId, _, _):
|
||||
message = context.account.postbox.messageAtId(messageId)
|
||||
case let .standaloneMessage(m):
|
||||
message = .single(m)
|
||||
case let .custom(messages, messageId, _):
|
||||
message = messages
|
||||
|> take(1)
|
||||
|> map { messages, _, _ in
|
||||
return messages.first(where: { $0.id == messageId })
|
||||
}
|
||||
}
|
||||
|
||||
let messageView = message
|
||||
|> filter({ $0 != nil })
|
||||
|> mapToSignal { message -> Signal<GalleryMessageHistoryView?, NoError> in
|
||||
switch source {
|
||||
case .peerMessagesAtId:
|
||||
case let .peerMessagesAtId(_, chatLocation, chatLocationContextHolder):
|
||||
if let tags = tagsForMessage(message!) {
|
||||
let namespaces: MessageIdNamespaces
|
||||
if Namespaces.Message.allScheduled.contains(message!.id.namespace) {
|
||||
@ -435,16 +437,27 @@ public class GalleryController: ViewController, StandalonePresentableController
|
||||
} else {
|
||||
namespaces = .not(Namespaces.Message.allScheduled)
|
||||
}
|
||||
return context.account.postbox.aroundMessageHistoryViewForLocation(.peer(message!.id.peerId), anchor: .index(message!.index), count: 50, clipHoles: false, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tags, namespaces: namespaces, orderStatistics: [.combinedLocation])
|
||||
|> mapToSignal { (view, _, _) -> Signal<GalleryMessageHistoryView?, NoError> in
|
||||
let mapped = GalleryMessageHistoryView.view(view)
|
||||
return .single(mapped)
|
||||
return context.account.postbox.aroundMessageHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), anchor: .index(message!.index), count: 50, clipHoles: false, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tags, namespaces: namespaces, orderStatistics: [.combinedLocation])
|
||||
|> mapToSignal { (view, _, _) -> Signal<GalleryMessageHistoryView?, NoError> in
|
||||
let mapped = GalleryMessageHistoryView.view(view)
|
||||
return .single(mapped)
|
||||
}
|
||||
} else {
|
||||
return .single(GalleryMessageHistoryView.single(MessageHistoryEntry(message: message!, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false))))
|
||||
}
|
||||
return .single(GalleryMessageHistoryView.entries([MessageHistoryEntry(message: message!, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false))], false, false))
|
||||
}
|
||||
case .standaloneMessage:
|
||||
return .single(GalleryMessageHistoryView.single(MessageHistoryEntry(message: message!, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false))))
|
||||
return .single(GalleryMessageHistoryView.entries([MessageHistoryEntry(message: message!, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false))], false ,false))
|
||||
case let .custom(messages, _, _):
|
||||
return messages
|
||||
|> map { messages, totalCount, hasMore in
|
||||
var entries: [MessageHistoryEntry] = []
|
||||
var index = messages.count
|
||||
for message in messages.reversed() {
|
||||
entries.append(MessageHistoryEntry(message: message, isRead: false, location: MessageHistoryEntryLocation(index: Int(totalCount) - index, count: Int(totalCount)), monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)))
|
||||
index -= 1
|
||||
}
|
||||
return GalleryMessageHistoryView.entries(entries, hasMore, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|> take(1)
|
||||
@ -470,7 +483,7 @@ public class GalleryController: ViewController, StandalonePresentableController
|
||||
loop: for i in 0 ..< entries.count {
|
||||
let message = entries[i].message
|
||||
switch source {
|
||||
case let .peerMessagesAtId(messageId):
|
||||
case let .peerMessagesAtId(messageId, _, _):
|
||||
if message.id == messageId {
|
||||
centralEntryStableId = message.stableId
|
||||
break loop
|
||||
@ -480,6 +493,11 @@ public class GalleryController: ViewController, StandalonePresentableController
|
||||
centralEntryStableId = message.stableId
|
||||
break loop
|
||||
}
|
||||
case let .custom(_, messageId, _):
|
||||
if message.id == messageId {
|
||||
centralEntryStableId = message.stableId
|
||||
break loop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -813,7 +831,7 @@ public class GalleryController: ViewController, StandalonePresentableController
|
||||
self.isOpaqueWhenInOverlay = true
|
||||
|
||||
switch source {
|
||||
case let .peerMessagesAtId(id):
|
||||
case let .peerMessagesAtId(id, _, _):
|
||||
if id.peerId.namespace == Namespaces.Peer.SecretChat {
|
||||
self.screenCaptureEventsDisposable = (screenCaptureEvents()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
@ -984,70 +1002,125 @@ public class GalleryController: ViewController, StandalonePresentableController
|
||||
}
|
||||
|
||||
switch strongSelf.source {
|
||||
case let .peerMessagesAtId(initialMessageId):
|
||||
var reloadAroundIndex: MessageIndex?
|
||||
if index <= 2 && strongSelf.hasLeftEntries {
|
||||
reloadAroundIndex = strongSelf.entries.first?.index
|
||||
} else if index >= strongSelf.entries.count - 3 && strongSelf.hasRightEntries {
|
||||
reloadAroundIndex = strongSelf.entries.last?.index
|
||||
}
|
||||
if let reloadAroundIndex = reloadAroundIndex, let tagMask = strongSelf.tagMask {
|
||||
let namespaces: MessageIdNamespaces
|
||||
if Namespaces.Message.allScheduled.contains(message.id.namespace) {
|
||||
namespaces = .just(Namespaces.Message.allScheduled)
|
||||
} else {
|
||||
namespaces = .not(Namespaces.Message.allScheduled)
|
||||
case let .peerMessagesAtId(_, chatLocation, chatLocationContextHolder):
|
||||
var reloadAroundIndex: MessageIndex?
|
||||
if index <= 2 && strongSelf.hasLeftEntries {
|
||||
reloadAroundIndex = strongSelf.entries.first?.index
|
||||
} else if index >= strongSelf.entries.count - 3 && strongSelf.hasRightEntries {
|
||||
reloadAroundIndex = strongSelf.entries.last?.index
|
||||
}
|
||||
let signal = strongSelf.context.account.postbox.aroundMessageHistoryViewForLocation(.peer(initialMessageId.peerId), anchor: .index(reloadAroundIndex), count: 50, clipHoles: false, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, namespaces: namespaces, orderStatistics: [.combinedLocation])
|
||||
|> mapToSignal { (view, _, _) -> Signal<GalleryMessageHistoryView?, NoError> in
|
||||
let mapped = GalleryMessageHistoryView.view(view)
|
||||
return .single(mapped)
|
||||
}
|
||||
|> take(1)
|
||||
|
||||
strongSelf.updateVisibleDisposable.set((signal
|
||||
|> deliverOnMainQueue).start(next: { view in
|
||||
guard let strongSelf = self, let view = view else {
|
||||
return
|
||||
}
|
||||
|
||||
let entries = view.entries
|
||||
|
||||
if strongSelf.invertItemOrder {
|
||||
strongSelf.entries = entries.reversed()
|
||||
strongSelf.hasLeftEntries = view.hasLater
|
||||
strongSelf.hasRightEntries = view.hasEarlier
|
||||
if let reloadAroundIndex = reloadAroundIndex, let tagMask = strongSelf.tagMask {
|
||||
let namespaces: MessageIdNamespaces
|
||||
if Namespaces.Message.allScheduled.contains(message.id.namespace) {
|
||||
namespaces = .just(Namespaces.Message.allScheduled)
|
||||
} else {
|
||||
strongSelf.entries = entries
|
||||
strongSelf.hasLeftEntries = view.hasEarlier
|
||||
strongSelf.hasRightEntries = view.hasLater
|
||||
namespaces = .not(Namespaces.Message.allScheduled)
|
||||
}
|
||||
if strongSelf.isViewLoaded {
|
||||
var items: [GalleryItem] = []
|
||||
var centralItemIndex: Int?
|
||||
for entry in strongSelf.entries {
|
||||
var isCentral = false
|
||||
if entry.message.stableId == strongSelf.centralEntryStableId {
|
||||
isCentral = true
|
||||
let signal = strongSelf.context.account.postbox.aroundMessageHistoryViewForLocation(strongSelf.context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), anchor: .index(reloadAroundIndex), count: 50, clipHoles: false, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, namespaces: namespaces, orderStatistics: [.combinedLocation])
|
||||
|> mapToSignal { (view, _, _) -> Signal<GalleryMessageHistoryView?, NoError> in
|
||||
let mapped = GalleryMessageHistoryView.view(view)
|
||||
return .single(mapped)
|
||||
}
|
||||
|> take(1)
|
||||
|
||||
strongSelf.updateVisibleDisposable.set((signal
|
||||
|> deliverOnMainQueue).start(next: { view in
|
||||
guard let strongSelf = self, let view = view else {
|
||||
return
|
||||
}
|
||||
if let item = galleryItemForEntry(context: strongSelf.context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: false, fromPlayingVideo: isCentral && strongSelf.fromPlayingVideo, landscape: isCentral && strongSelf.landscape, timecode: isCentral ? strongSelf.timecode : nil, configuration: strongSelf.configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _ in }, present: { [weak self] c, a in
|
||||
if let strongSelf = self {
|
||||
strongSelf.presentInGlobalOverlay(c, with: a)
|
||||
|
||||
let entries = view.entries
|
||||
|
||||
if strongSelf.invertItemOrder {
|
||||
strongSelf.entries = entries.reversed()
|
||||
strongSelf.hasLeftEntries = view.hasLater
|
||||
strongSelf.hasRightEntries = view.hasEarlier
|
||||
} else {
|
||||
strongSelf.entries = entries
|
||||
strongSelf.hasLeftEntries = view.hasEarlier
|
||||
strongSelf.hasRightEntries = view.hasLater
|
||||
}
|
||||
if strongSelf.isViewLoaded {
|
||||
var items: [GalleryItem] = []
|
||||
var centralItemIndex: Int?
|
||||
for entry in strongSelf.entries {
|
||||
var isCentral = false
|
||||
if entry.message.stableId == strongSelf.centralEntryStableId {
|
||||
isCentral = true
|
||||
}
|
||||
if let item = galleryItemForEntry(context: strongSelf.context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: false, fromPlayingVideo: isCentral && strongSelf.fromPlayingVideo, landscape: isCentral && strongSelf.landscape, timecode: isCentral ? strongSelf.timecode : nil, configuration: strongSelf.configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _ in }, present: { [weak self] c, a in
|
||||
if let strongSelf = self {
|
||||
strongSelf.presentInGlobalOverlay(c, with: a)
|
||||
}
|
||||
}) {
|
||||
if isCentral {
|
||||
centralItemIndex = items.count
|
||||
}
|
||||
items.append(item)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
if isCentral {
|
||||
centralItemIndex = items.count
|
||||
|
||||
strongSelf.galleryNode.pager.replaceItems(items, centralItemIndex: centralItemIndex)
|
||||
}
|
||||
}))
|
||||
}
|
||||
case let .custom(messages, _, loadMore):
|
||||
if index >= strongSelf.entries.count - 3 && strongSelf.hasRightEntries && !strongSelf.loadingMore {
|
||||
strongSelf.loadingMore = true
|
||||
loadMore?()
|
||||
|
||||
strongSelf.updateVisibleDisposable.set((messages
|
||||
|> deliverOnMainQueue).start(next: { messages, totalCount, hasMore in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
var entries: [MessageHistoryEntry] = []
|
||||
var index = messages.count
|
||||
for message in messages.reversed() {
|
||||
entries.append(MessageHistoryEntry(message: message, isRead: false, location: MessageHistoryEntryLocation(index: Int(totalCount) - index, count: Int(totalCount)), monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)))
|
||||
index -= 1
|
||||
}
|
||||
|
||||
if entries.count > strongSelf.entries.count {
|
||||
if strongSelf.invertItemOrder {
|
||||
strongSelf.entries = entries.reversed()
|
||||
strongSelf.hasLeftEntries = false
|
||||
strongSelf.hasRightEntries = hasMore
|
||||
} else {
|
||||
strongSelf.entries = entries
|
||||
strongSelf.hasLeftEntries = hasMore
|
||||
strongSelf.hasRightEntries = false
|
||||
}
|
||||
if strongSelf.isViewLoaded {
|
||||
var items: [GalleryItem] = []
|
||||
var centralItemIndex: Int?
|
||||
for entry in strongSelf.entries {
|
||||
var isCentral = false
|
||||
if entry.message.stableId == strongSelf.centralEntryStableId {
|
||||
isCentral = true
|
||||
}
|
||||
if let item = galleryItemForEntry(context: strongSelf.context, presentationData: strongSelf.presentationData, entry: entry, isCentral: isCentral, streamVideos: false, fromPlayingVideo: isCentral && strongSelf.fromPlayingVideo, landscape: isCentral && strongSelf.landscape, timecode: isCentral ? strongSelf.timecode : nil, configuration: strongSelf.configuration, performAction: strongSelf.performAction, openActionOptions: strongSelf.openActionOptions, storeMediaPlaybackState: strongSelf.actionInteraction?.storeMediaPlaybackState ?? { _, _ in }, present: { [weak self] c, a in
|
||||
if let strongSelf = self {
|
||||
strongSelf.presentInGlobalOverlay(c, with: a)
|
||||
}
|
||||
}) {
|
||||
if isCentral {
|
||||
centralItemIndex = items.count
|
||||
}
|
||||
items.append(item)
|
||||
}
|
||||
}
|
||||
items.append(item)
|
||||
|
||||
strongSelf.galleryNode.pager.replaceItems(items, centralItemIndex: centralItemIndex)
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.galleryNode.pager.replaceItems(items, centralItemIndex: centralItemIndex)
|
||||
}
|
||||
}))
|
||||
}
|
||||
default:
|
||||
break
|
||||
strongSelf.loadingMore = false
|
||||
}))
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
if strongSelf.didSetReady {
|
||||
@ -1152,6 +1225,7 @@ public class GalleryController: ViewController, StandalonePresentableController
|
||||
self.adjustedForInitialPreviewingLayout = true
|
||||
self.galleryNode.setControlsHidden(true, animated: false)
|
||||
if let centralItemNode = self.galleryNode.pager.centralItemNode(), let itemSize = centralItemNode.contentSize() {
|
||||
centralItemNode.adjustForPreviewing()
|
||||
self.preferredContentSize = itemSize.aspectFitted(layout.size)
|
||||
self.containerLayoutUpdated(ContainerViewLayout(size: self.preferredContentSize, metrics: LayoutMetrics(), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: .immediate)
|
||||
}
|
||||
|
||||
@ -85,6 +85,9 @@ open class GalleryItemNode: ASDisplayNode {
|
||||
open func controlsVisibilityUpdated(isVisible: Bool) {
|
||||
}
|
||||
|
||||
open func adjustForPreviewing() {
|
||||
}
|
||||
|
||||
open func animateIn(from node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) {
|
||||
}
|
||||
|
||||
|
||||
@ -1352,7 +1352,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
|
||||
switch contentInfo {
|
||||
case let .message(message):
|
||||
let gallery = GalleryController(context: context, source: .peerMessagesAtId(message.id), replaceRootController: { controller, ready in
|
||||
let gallery = GalleryController(context: context, source: .peerMessagesAtId(messageId: message.id, chatLocation: .peer(message.id.peerId), chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil)), replaceRootController: { controller, ready in
|
||||
if let baseNavigationController = baseNavigationController {
|
||||
baseNavigationController.replaceTopController(controller, animated: false, ready: ready)
|
||||
}
|
||||
@ -1434,7 +1434,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
|
||||
switch contentInfo {
|
||||
case let .message(message):
|
||||
let gallery = GalleryController(context: context, source: .peerMessagesAtId(message.id), replaceRootController: { controller, ready in
|
||||
let gallery = GalleryController(context: context, source: .peerMessagesAtId(messageId: message.id, chatLocation: .peer(message.id.peerId), chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil)), replaceRootController: { controller, ready in
|
||||
if let baseNavigationController = baseNavigationController {
|
||||
baseNavigationController.replaceTopController(controller, animated: false, ready: ready)
|
||||
}
|
||||
@ -1537,6 +1537,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func adjustForPreviewing() {
|
||||
super.adjustForPreviewing()
|
||||
|
||||
self.scrubberView.isHidden = true
|
||||
}
|
||||
|
||||
override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> {
|
||||
return .single((self.footerContentNode, nil))
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ static_library(
|
||||
"//submodules/TelegramBaseController:TelegramBaseController",
|
||||
"//submodules/ChatListUI:ChatListUI",
|
||||
"//submodules/SegmentedControlNode:SegmentedControlNode",
|
||||
"//submodules/ListMessageItem:ListMessageItem",
|
||||
],
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||
|
||||
@ -18,6 +18,7 @@ swift_library(
|
||||
"//submodules/TelegramBaseController:TelegramBaseController",
|
||||
"//submodules/ChatListUI:ChatListUI",
|
||||
"//submodules/SegmentedControlNode:SegmentedControlNode",
|
||||
"//submodules/ListMessageItem:ListMessageItem",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -9,6 +9,7 @@ import TelegramPresentationData
|
||||
import TelegramBaseController
|
||||
import AccountContext
|
||||
import ChatListUI
|
||||
import ListMessageItem
|
||||
|
||||
public final class HashtagSearchController: TelegramBaseController {
|
||||
private let queue = Queue()
|
||||
@ -41,11 +42,11 @@ public final class HashtagSearchController: TelegramBaseController {
|
||||
|
||||
let chatListPresentationData = ChatListPresentationData(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: self.presentationData.disableAnimations)
|
||||
|
||||
let location: SearchMessagesLocation = .general(tags: nil)
|
||||
let location: SearchMessagesLocation = .general(tags: nil, minDate: nil, maxDate: nil)
|
||||
let search = searchMessages(account: context.account, location: location, query: query, state: nil)
|
||||
let foundMessages: Signal<[ChatListSearchEntry], NoError> = search
|
||||
|> map { result, _ in
|
||||
return result.messages.map({ .message($0, RenderedPeer(message: $0), result.readStates[$0.id.peerId], chatListPresentationData) })
|
||||
return result.messages.map({ .message($0, RenderedPeer(message: $0), result.readStates[$0.id.peerId], chatListPresentationData, result.totalCount, nil, false) })
|
||||
}
|
||||
let interaction = ChatListNodeInteraction(activateSearch: {
|
||||
}, peerSelected: { _, _ in
|
||||
@ -82,10 +83,27 @@ public final class HashtagSearchController: TelegramBaseController {
|
||||
if let strongSelf = self {
|
||||
let previousEntries = previousSearchItems.swap(entries)
|
||||
|
||||
let firstTime = previousEntries == nil
|
||||
let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries, displayingResults: true, context: strongSelf.context, presentationData: strongSelf.presentationData, enableHeaders: false, filter: [], interaction: interaction, peerContextAction: nil, toggleExpandLocalResults: {
|
||||
}, toggleExpandGlobalResults: {
|
||||
let listInteraction = ListMessageItemInteraction(openMessage: { message, mode -> Bool in
|
||||
return true
|
||||
}, openMessageContextMenu: { message, bool, node, rect, gesture in
|
||||
|
||||
}, toggleMessagesSelection: { messageId, selected in
|
||||
|
||||
}, openUrl: { url, _, _, message in
|
||||
}, openInstantPage: { message, data in
|
||||
|
||||
}, longTap: { action, message in
|
||||
|
||||
}, getHiddenMedia: {
|
||||
return [:]
|
||||
})
|
||||
|
||||
let firstTime = previousEntries == nil
|
||||
let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries, displayingResults: true, isEmpty: entries.isEmpty, isLoading: false, animated: false, searchQuery: "", context: strongSelf.context, presentationData: strongSelf.presentationData, enableHeaders: false, filter: [], interaction: interaction, listInteraction: listInteraction, peerContextAction: nil, toggleExpandLocalResults: {
|
||||
}, toggleExpandGlobalResults: {
|
||||
}, searchPeer: { _ in
|
||||
|
||||
}, searchResults: [], searchOptions: nil, messageContextAction: nil)
|
||||
strongSelf.controllerNode.enqueueTransition(transition, firstTime: firstTime)
|
||||
}
|
||||
})
|
||||
|
||||
@ -42,6 +42,8 @@ final class HashtagSearchControllerNode: ASDisplayNode {
|
||||
var items: [String] = []
|
||||
if peer?.id == context.account.peerId {
|
||||
items.append(presentationData.strings.Conversation_SavedMessages)
|
||||
} else if let id = peer?.id, id.isReplies {
|
||||
items.append(presentationData.strings.DialogList_Replies)
|
||||
} else {
|
||||
items.append(peer?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) ?? "")
|
||||
}
|
||||
|
||||
@ -9,6 +9,56 @@ import TelegramPresentationData
|
||||
import TelegramUIPreferences
|
||||
import AccountContext
|
||||
|
||||
public func instantPageAndAnchor(message: Message) -> (TelegramMediaWebpage, String?)? {
|
||||
for media in message.media {
|
||||
if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
|
||||
if let _ = content.instantPage {
|
||||
var textUrl: String?
|
||||
if let pageUrl = URL(string: content.url) {
|
||||
inner: for attribute in message.attributes {
|
||||
if let attribute = attribute as? TextEntitiesMessageAttribute {
|
||||
for entity in attribute.entities {
|
||||
switch entity.type {
|
||||
case let .TextUrl(url):
|
||||
if let parsedUrl = URL(string: url) {
|
||||
if pageUrl.scheme == parsedUrl.scheme && pageUrl.host == parsedUrl.host && pageUrl.path == parsedUrl.path {
|
||||
textUrl = url
|
||||
}
|
||||
}
|
||||
case .Url:
|
||||
let nsText = message.text as NSString
|
||||
var entityRange = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
|
||||
if entityRange.location + entityRange.length > nsText.length {
|
||||
entityRange.location = max(0, nsText.length - entityRange.length)
|
||||
entityRange.length = nsText.length - entityRange.location
|
||||
}
|
||||
let url = nsText.substring(with: entityRange)
|
||||
if let parsedUrl = URL(string: url) {
|
||||
if pageUrl.scheme == parsedUrl.scheme && pageUrl.host == parsedUrl.host && pageUrl.path == parsedUrl.path {
|
||||
textUrl = url
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
break inner
|
||||
}
|
||||
}
|
||||
}
|
||||
var anchor: String?
|
||||
if let textUrl = textUrl, let anchorRange = textUrl.range(of: "#") {
|
||||
anchor = String(textUrl[anchorRange.upperBound...])
|
||||
}
|
||||
|
||||
return (webpage, anchor)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public final class InstantPageController: ViewController {
|
||||
private let context: AccountContext
|
||||
private var webPage: TelegramMediaWebpage
|
||||
|
||||
@ -855,6 +855,9 @@ func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, boundingWidth:
|
||||
if let image = loadedContent.image, let id = image.id {
|
||||
media[id] = image
|
||||
}
|
||||
if let video = loadedContent.file, let id = video.id {
|
||||
media[id] = video
|
||||
}
|
||||
|
||||
var mediaIndexCounter: Int = 0
|
||||
var embedIndexCounter: Int = 0
|
||||
|
||||
@ -683,6 +683,8 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
|
||||
|
||||
if item.peer.id == item.context.account.peerId, case .threatSelfAsSaved = item.aliasHandling {
|
||||
titleAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_SavedMessages, font: currentBoldFont, textColor: titleColor)
|
||||
} else if item.peer.id.isReplies {
|
||||
titleAttributedString = NSAttributedString(string: item.presentationData.strings.DialogList_Replies, font: currentBoldFont, textColor: titleColor)
|
||||
} else if let user = item.peer as? TelegramUser {
|
||||
if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty {
|
||||
let string = NSMutableAttributedString()
|
||||
@ -1059,6 +1061,8 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
|
||||
|
||||
if item.peer.id == item.context.account.peerId, case .threatSelfAsSaved = item.aliasHandling {
|
||||
strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: item.peer, overrideImage: .savedMessagesIcon, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoad)
|
||||
} else if item.peer.id.isReplies {
|
||||
strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: item.peer, overrideImage: .repliesIcon, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: synchronousLoad)
|
||||
} else {
|
||||
var overrideImage: AvatarNodeImageOverride?
|
||||
if item.peer.isDeleted {
|
||||
|
||||
@ -570,7 +570,7 @@ private func loadLegacyMessages(account: TemporaryAccount, basePath: String, acc
|
||||
}
|
||||
|
||||
let (parsedTags, parsedGlobalTags) = tagsForStoreMessage(incoming: parsedFlags.contains(.Incoming), attributes: parsedAttributes, media: parsedMedia, textEntities: nil)
|
||||
messages.append(StoreMessage(id: parsedId, globallyUniqueId: globallyUniqueId, groupingKey: parsedGroupingKey, timestamp: timestamp, flags: parsedFlags, tags: parsedTags, globalTags: parsedGlobalTags, localTags: [], forwardInfo: nil, authorId: parsedAuthorId, text: text, attributes: parsedAttributes, media: parsedMedia))
|
||||
messages.append(StoreMessage(id: parsedId, globallyUniqueId: globallyUniqueId, groupingKey: parsedGroupingKey, threadId: nil, timestamp: timestamp, flags: parsedFlags, tags: parsedTags, globalTags: parsedGlobalTags, localTags: [], forwardInfo: nil, authorId: parsedAuthorId, text: text, attributes: parsedAttributes, media: parsedMedia))
|
||||
|
||||
//Logger.shared.log("loadLegacyMessages", "message \(messageId) completed")
|
||||
|
||||
|
||||
36
submodules/ListMessageItem/BUCK
Normal file
36
submodules/ListMessageItem/BUCK
Normal file
@ -0,0 +1,36 @@
|
||||
load("//Config:buck_rule_macros.bzl", "static_library")
|
||||
|
||||
static_library(
|
||||
name = "ListMessageItem",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit#shared",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit#shared",
|
||||
"//submodules/Display:Display#shared",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/ItemListUI:ItemListUI",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
|
||||
"//submodules/ListSectionHeaderNode:ListSectionHeaderNode",
|
||||
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
|
||||
"//submodules/UrlHandling:UrlHandling",
|
||||
"//submodules/UrlWhitelist:UrlWhitelist",
|
||||
"//submodules/WebsiteType:WebsiteType",
|
||||
"//submodules/PhotoResources:PhotoResources",
|
||||
"//submodules/RadialStatusNode:RadialStatusNode",
|
||||
"//submodules/SemanticStatusNode:SemanticStatusNode",
|
||||
"//submodules/MusicAlbumArtResources:MusicAlbumArtResources",
|
||||
"//submodules/MediaPlayer:UniversalMediaPlayer",
|
||||
"//submodules/ContextUI:ContextUI",
|
||||
"//submodules/FileMediaResourceStatus:FileMediaResourceStatus",
|
||||
],
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||
"$SDKROOT/System/Library/Frameworks/UIKit.framework",
|
||||
],
|
||||
)
|
||||
38
submodules/ListMessageItem/BUILD
Normal file
38
submodules/ListMessageItem/BUILD
Normal file
@ -0,0 +1,38 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ListMessageItem",
|
||||
module_name = "ListMessageItem",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/SyncCore:SyncCore",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/ItemListUI:ItemListUI",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
|
||||
"//submodules/ListSectionHeaderNode:ListSectionHeaderNode",
|
||||
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
|
||||
"//submodules/UrlHandling:UrlHandling",
|
||||
"//submodules/UrlWhitelist:UrlWhitelist",
|
||||
"//submodules/WebsiteType:WebsiteType",
|
||||
"//submodules/PhotoResources:PhotoResources",
|
||||
"//submodules/RadialStatusNode:RadialStatusNode",
|
||||
"//submodules/SemanticStatusNode:SemanticStatusNode",
|
||||
"//submodules/MusicAlbumArtResources:MusicAlbumArtResources",
|
||||
"//submodules/MediaPlayer:UniversalMediaPlayer",
|
||||
"//submodules/ContextUI:ContextUI",
|
||||
"//submodules/FileMediaResourceStatus:FileMediaResourceStatus",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
@ -16,7 +16,7 @@ private let timezoneOffset: Int32 = {
|
||||
return Int32(timeinfoNow.tm_gmtoff)
|
||||
}()
|
||||
|
||||
func listMessageDateHeaderId(timestamp: Int32) -> Int64 {
|
||||
public func listMessageDateHeaderId(timestamp: Int32) -> Int64 {
|
||||
let unclippedValue: Int64 = min(Int64(Int32.max), Int64(timestamp) + Int64(timezoneOffset))
|
||||
|
||||
var time: time_t = time_t(Int32(clamping: unclippedValue))
|
||||
@ -28,7 +28,7 @@ func listMessageDateHeaderId(timestamp: Int32) -> Int64 {
|
||||
return Int64(roundedTimestamp)
|
||||
}
|
||||
|
||||
func listMessageDateHeaderInfo(timestamp: Int32) -> (year: Int32, month: Int32) {
|
||||
public func listMessageDateHeaderInfo(timestamp: Int32) -> (year: Int32, month: Int32) {
|
||||
var time: time_t = time_t(timestamp + timezoneOffset)
|
||||
var timeinfo: tm = tm()
|
||||
localtime_r(&time, &timeinfo)
|
||||
@ -76,7 +76,7 @@ final class ListMessageDateHeader: ListViewItemHeader {
|
||||
}
|
||||
}
|
||||
|
||||
final class ListMessageDateHeaderNode: ListViewItemHeaderNode {
|
||||
public final class ListMessageDateHeaderNode: ListViewItemHeaderNode {
|
||||
var theme: PresentationTheme
|
||||
var strings: PresentationStrings
|
||||
let headerNode: ListSectionHeaderNode
|
||||
@ -99,7 +99,7 @@ final class ListMessageDateHeaderNode: ListViewItemHeaderNode {
|
||||
self.headerNode.title = stringForMonth(strings: strings, month: month, ofYear: year).uppercased()
|
||||
}
|
||||
|
||||
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
public func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.theme = theme
|
||||
self.headerNode.updateTheme(theme: theme)
|
||||
|
||||
@ -109,7 +109,7 @@ final class ListMessageDateHeaderNode: ListViewItemHeaderNode {
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) {
|
||||
override public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) {
|
||||
let headerFrame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: size.width, height: size.height + UIScreenPixel))
|
||||
self.headerNode.frame = headerFrame
|
||||
self.headerNode.updateLayout(size: headerFrame.size, leftInset: leftInset, rightInset: rightInset)
|
||||
@ -18,6 +18,7 @@ import PhotoResources
|
||||
import MusicAlbumArtResources
|
||||
import UniversalMediaPlayer
|
||||
import ContextUI
|
||||
import FileMediaResourceStatus
|
||||
|
||||
private let extensionImageCache = Atomic<[UInt32: UIImage]>(value: [:])
|
||||
|
||||
@ -88,6 +89,36 @@ private func extensionImage(fileExtension: String?) -> UIImage? {
|
||||
}
|
||||
private let extensionFont = Font.with(size: 15.0, design: .round, traits: [.bold])
|
||||
|
||||
func fullAuthorString(for item: ListMessageItem) -> String {
|
||||
var authorString = ""
|
||||
if let author = item.message.author, [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(item.message.id.peerId.namespace) {
|
||||
var authorName = ""
|
||||
if author.id == item.context.account.peerId {
|
||||
authorName = item.presentationData.strings.DialogList_You
|
||||
} else {
|
||||
authorName = author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
|
||||
}
|
||||
if let peer = item.message.peers[item.message.id.peerId], author.id != peer.id {
|
||||
authorString = "\(authorName) → \(peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder))"
|
||||
} else {
|
||||
authorString = authorName
|
||||
}
|
||||
} else if let peer = item.message.peers[item.message.id.peerId] {
|
||||
if item.message.id.peerId.namespace == Namespaces.Peer.CloudChannel {
|
||||
authorString = peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
|
||||
} else {
|
||||
if item.message.id.peerId == item.context.account.peerId {
|
||||
authorString = item.presentationData.strings.DialogList_SavedMessages
|
||||
} else if item.message.flags.contains(.Incoming) {
|
||||
authorString = peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
|
||||
} else {
|
||||
authorString = "\(item.presentationData.strings.DialogList_You) → \(peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder))"
|
||||
}
|
||||
}
|
||||
}
|
||||
return authorString
|
||||
}
|
||||
|
||||
private struct FetchControls {
|
||||
let fetch: () -> Void
|
||||
let cancel: () -> Void
|
||||
@ -122,7 +153,7 @@ private enum FileIconImage: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
final class ListMessageFileItemNode: ListMessageNode {
|
||||
public final class ListMessageFileItemNode: ListMessageNode {
|
||||
private let contextSourceNode: ContextExtractedContentContainingNode
|
||||
private let containerNode: ContextControllerSourceNode
|
||||
private let extractedBackgroundImageNode: ASImageNode
|
||||
@ -140,6 +171,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
private let titleNode: TextNode
|
||||
private let descriptionNode: TextNode
|
||||
private let descriptionProgressNode: ImmediateTextNode
|
||||
private let dateNode: TextNode
|
||||
|
||||
private let extensionIconNode: ASImageNode
|
||||
private let extensionIconText: TextNode
|
||||
@ -195,6 +227,9 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
self.descriptionProgressNode.isUserInteractionEnabled = false
|
||||
self.descriptionProgressNode.maximumNumberOfLines = 1
|
||||
|
||||
self.dateNode = TextNode()
|
||||
self.dateNode.isUserInteractionEnabled = false
|
||||
|
||||
self.extensionIconNode = ASImageNode()
|
||||
self.extensionIconNode.isLayerBacked = true
|
||||
self.extensionIconNode.displaysAsynchronously = false
|
||||
@ -228,6 +263,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
self.offsetContainerNode.addSubnode(self.titleNode)
|
||||
self.offsetContainerNode.addSubnode(self.descriptionNode)
|
||||
self.offsetContainerNode.addSubnode(self.descriptionProgressNode)
|
||||
self.offsetContainerNode.addSubnode(self.dateNode)
|
||||
self.offsetContainerNode.addSubnode(self.extensionIconNode)
|
||||
self.offsetContainerNode.addSubnode(self.extensionIconText)
|
||||
self.offsetContainerNode.addSubnode(self.iconStatusNode)
|
||||
@ -237,7 +273,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
return
|
||||
}
|
||||
|
||||
item.controllerInteraction.openMessageContextMenu(item.message, false, strongSelf.contextSourceNode, strongSelf.contextSourceNode.bounds, gesture)
|
||||
item.interaction.openMessageContextMenu(item.message, false, strongSelf.contextSourceNode, strongSelf.contextSourceNode.bounds, gesture)
|
||||
}
|
||||
|
||||
self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
|
||||
@ -246,7 +282,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
}
|
||||
|
||||
if isExtracted {
|
||||
strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: item.theme.list.plainBackgroundColor)
|
||||
strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: item.presentationData.theme.theme.list.plainBackgroundColor)
|
||||
}
|
||||
|
||||
if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect {
|
||||
@ -260,6 +296,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
self?.extractedBackgroundImageNode.image = nil
|
||||
}
|
||||
})
|
||||
transition.updateAlpha(node: strongSelf.dateNode, alpha: isExtracted ? 0.0 : 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
@ -294,14 +331,15 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
self.addTransitionOffsetAnimation(0.0, duration: duration, beginAt: currentTimestamp)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
override func asyncLayout() -> (_ item: ListMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
|
||||
override public func asyncLayout() -> (_ item: ListMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
|
||||
let titleNodeMakeLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let descriptionNodeMakeLayout = TextNode.asyncLayout(self.descriptionNode)
|
||||
let extensionIconTextMakeLayout = TextNode.asyncLayout(self.extensionIconText)
|
||||
let dateNodeMakeLayout = TextNode.asyncLayout(self.dateNode)
|
||||
let iconImageLayout = self.iconImageNode.asyncLayout()
|
||||
|
||||
let currentMedia = self.currentMedia
|
||||
@ -315,13 +353,14 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
return { [weak self] item, params, _, _, dateHeaderAtBottom in
|
||||
var updatedTheme: PresentationTheme?
|
||||
|
||||
if currentItem?.theme !== item.theme {
|
||||
updatedTheme = item.theme
|
||||
if currentItem?.presentationData.theme.theme !== item.presentationData.theme.theme {
|
||||
updatedTheme = item.presentationData.theme.theme
|
||||
}
|
||||
|
||||
let titleFont = Font.semibold(floor(item.fontSize.baseDisplaySize * 16.0 / 17.0))
|
||||
let audioTitleFont = Font.semibold(floor(item.fontSize.baseDisplaySize * 16.0 / 17.0))
|
||||
let descriptionFont = Font.regular(floor(item.fontSize.baseDisplaySize * 14.0 / 17.0))
|
||||
let titleFont = Font.semibold(floor(item.presentationData.fontSize.baseDisplaySize * 16.0 / 17.0))
|
||||
let audioTitleFont = Font.semibold(floor(item.presentationData.fontSize.baseDisplaySize * 16.0 / 17.0))
|
||||
let descriptionFont = Font.regular(floor(item.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0))
|
||||
let dateFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0))
|
||||
|
||||
var leftInset: CGFloat = 65.0 + params.leftInset
|
||||
let rightInset: CGFloat = 8.0 + params.rightInset
|
||||
@ -329,7 +368,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
var leftOffset: CGFloat = 0.0
|
||||
var selectionNodeWidthAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)?
|
||||
if case let .selectable(selected) = item.selection {
|
||||
let (selectionWidth, selectionApply) = selectionNodeLayout(item.theme.list.itemCheckColors.strokeColor, item.theme.list.itemCheckColors.fillColor, item.theme.list.itemCheckColors.foregroundColor, selected, false)
|
||||
let (selectionWidth, selectionApply) = selectionNodeLayout(item.presentationData.theme.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.theme.list.itemCheckColors.fillColor, item.presentationData.theme.theme.list.itemCheckColors.foregroundColor, selected, false)
|
||||
selectionNodeWidthAndApply = (selectionWidth, selectionApply)
|
||||
leftOffset += selectionWidth
|
||||
}
|
||||
@ -363,61 +402,85 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
isAudio = true
|
||||
isVoice = voice
|
||||
|
||||
titleText = NSAttributedString(string: title ?? (file.fileName ?? "Unknown Track"), font: audioTitleFont, textColor: item.theme.list.itemPrimaryTextColor)
|
||||
titleText = NSAttributedString(string: title ?? (file.fileName ?? "Unknown Track"), font: audioTitleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
|
||||
|
||||
let descriptionString: String
|
||||
var descriptionString: String
|
||||
if let performer = performer {
|
||||
descriptionString = "\(stringForDuration(Int32(duration))) • \(performer)"
|
||||
if item.isGlobalSearchResult {
|
||||
descriptionString = performer
|
||||
} else {
|
||||
descriptionString = "\(stringForDuration(Int32(duration))) • \(performer)"
|
||||
}
|
||||
} else if let size = file.size {
|
||||
descriptionString = dataSizeString(size, decimalSeparator: item.dateTimeFormat.decimalSeparator)
|
||||
descriptionString = dataSizeString(size, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator)
|
||||
} else {
|
||||
descriptionString = ""
|
||||
}
|
||||
|
||||
descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor)
|
||||
if item.isGlobalSearchResult {
|
||||
let authorString = fullAuthorString(for: item)
|
||||
if descriptionString.isEmpty {
|
||||
descriptionString = authorString
|
||||
} else {
|
||||
descriptionString = "\(descriptionString) • \(authorString)"
|
||||
}
|
||||
}
|
||||
|
||||
descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
|
||||
|
||||
if !voice {
|
||||
iconImage = .albumArt(file, SharedMediaPlaybackAlbumArt(thumbnailResource: ExternalMusicAlbumArtResource(title: title ?? "", performer: performer ?? "", isThumbnail: true), fullSizeResource: ExternalMusicAlbumArtResource(title: title ?? "", performer: performer ?? "", isThumbnail: false)))
|
||||
} else {
|
||||
titleText = NSAttributedString(string: " ", font: audioTitleFont, textColor: item.theme.list.itemPrimaryTextColor)
|
||||
descriptionText = NSAttributedString(string: item.message.author?.displayTitle(strings: item.strings, displayOrder: .firstLast) ?? " ", font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor)
|
||||
titleText = NSAttributedString(string: " ", font: audioTitleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
|
||||
descriptionText = NSAttributedString(string: item.message.author?.displayTitle(strings: item.presentationData.strings, displayOrder: .firstLast) ?? " ", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isInstantVideo || isVoice {
|
||||
let authorName: String
|
||||
var authorName: String
|
||||
if let author = message.forwardInfo?.author {
|
||||
if author.id == item.context.account.peerId {
|
||||
authorName = item.strings.DialogList_You
|
||||
authorName = item.presentationData.strings.DialogList_You
|
||||
} else {
|
||||
authorName = author.displayTitle(strings: item.strings, displayOrder: .firstLast)
|
||||
authorName = author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
|
||||
}
|
||||
} else if let signature = message.forwardInfo?.authorSignature {
|
||||
authorName = signature
|
||||
} else if let author = message.author {
|
||||
if author.id == item.context.account.peerId {
|
||||
authorName = item.strings.DialogList_You
|
||||
authorName = item.presentationData.strings.DialogList_You
|
||||
} else {
|
||||
authorName = author.displayTitle(strings: item.strings, displayOrder: .firstLast)
|
||||
authorName = author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
|
||||
}
|
||||
} else {
|
||||
authorName = " "
|
||||
}
|
||||
titleText = NSAttributedString(string: authorName, font: audioTitleFont, textColor: item.theme.list.itemPrimaryTextColor)
|
||||
let dateString = stringForFullDate(timestamp: item.message.timestamp, strings: item.strings, dateTimeFormat: item.dateTimeFormat)
|
||||
let descriptionString: String
|
||||
if let duration = file.duration {
|
||||
descriptionString = "\(stringForDuration(Int32(duration))) • \(dateString)"
|
||||
} else {
|
||||
descriptionString = dateString
|
||||
|
||||
if item.isGlobalSearchResult {
|
||||
authorName = fullAuthorString(for: item)
|
||||
}
|
||||
|
||||
descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor)
|
||||
titleText = NSAttributedString(string: authorName, font: audioTitleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
|
||||
let dateString = stringForFullDate(timestamp: item.message.timestamp, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat)
|
||||
var descriptionString: String = ""
|
||||
if let duration = file.duration {
|
||||
if item.isGlobalSearchResult {
|
||||
descriptionString = stringForDuration(Int32(duration))
|
||||
} else {
|
||||
descriptionString = "\(stringForDuration(Int32(duration))) • \(dateString)"
|
||||
}
|
||||
} else {
|
||||
if !item.isGlobalSearchResult {
|
||||
descriptionString = dateString
|
||||
}
|
||||
}
|
||||
|
||||
descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
|
||||
iconImage = .roundVideo(file)
|
||||
} else if !isAudio {
|
||||
let fileName: String = file.fileName ?? ""
|
||||
titleText = NSAttributedString(string: fileName, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor)
|
||||
titleText = NSAttributedString(string: fileName, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
|
||||
|
||||
var fileExtension: String?
|
||||
if let range = fileName.range(of: ".", options: [.backwards]) {
|
||||
@ -432,16 +495,31 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
iconImage = .imageRepresentation(file, representation)
|
||||
}
|
||||
|
||||
let dateString = stringForFullDate(timestamp: item.message.timestamp, strings: item.strings, dateTimeFormat: item.dateTimeFormat)
|
||||
let dateString = stringForFullDate(timestamp: item.message.timestamp, strings: item.presentationData.strings, dateTimeFormat: item.presentationData.dateTimeFormat)
|
||||
|
||||
let descriptionString: String
|
||||
var descriptionString: String = ""
|
||||
if let size = file.size {
|
||||
descriptionString = "\(dataSizeString(size, decimalSeparator: item.dateTimeFormat.decimalSeparator)) • \(dateString)"
|
||||
if item.isGlobalSearchResult {
|
||||
descriptionString = (dataSizeString(size, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator))
|
||||
} else {
|
||||
descriptionString = "\(dataSizeString(size, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator)) • \(dateString)"
|
||||
}
|
||||
} else {
|
||||
descriptionString = "\(dateString)"
|
||||
if !item.isGlobalSearchResult {
|
||||
descriptionString = "\(dateString)"
|
||||
}
|
||||
}
|
||||
|
||||
if item.isGlobalSearchResult {
|
||||
let authorString = fullAuthorString(for: item)
|
||||
if descriptionString.isEmpty {
|
||||
descriptionString = authorString
|
||||
} else {
|
||||
descriptionString = "\(descriptionString) • \(authorString)"
|
||||
}
|
||||
}
|
||||
|
||||
descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor)
|
||||
descriptionText = NSAttributedString(string: descriptionString, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
|
||||
}
|
||||
|
||||
break
|
||||
@ -478,7 +556,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
}
|
||||
|
||||
if statusUpdated {
|
||||
updatedStatusSignal = messageFileMediaResourceStatus(context: item.context, file: selectedMedia, message: message, isRecentActions: false, isSharedMedia: true)
|
||||
updatedStatusSignal = messageFileMediaResourceStatus(context: item.context, file: selectedMedia, message: message, isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult)
|
||||
|
||||
if isAudio || isInstantVideo {
|
||||
if let currentUpdatedStatusSignal = updatedStatusSignal {
|
||||
@ -494,14 +572,20 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
}
|
||||
}
|
||||
if isVoice {
|
||||
updatedPlaybackStatusSignal = messageFileMediaPlaybackStatus(context: item.context, file: selectedMedia, message: message, isRecentActions: false)
|
||||
updatedPlaybackStatusSignal = messageFileMediaPlaybackStatus(context: item.context, file: selectedMedia, message: message, isRecentActions: false, isGlobalSearch: item.isGlobalSearchResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(TextNodeLayoutArguments(attributedString: titleText, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 40.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
|
||||
let dateText = stringForRelativeTimestamp(strings: item.presentationData.strings, relativeTimestamp: item.message.timestamp, relativeTo: timestamp, dateTimeFormat: item.presentationData.dateTimeFormat)
|
||||
let dateAttributedString = NSAttributedString(string: dateText, font: dateFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
|
||||
|
||||
let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(TextNodeLayoutArguments(attributedString: descriptionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 12.0 - 40.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let (dateNodeLayout, dateNodeApply) = dateNodeMakeLayout(TextNodeLayoutArguments(attributedString: dateAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 12.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(TextNodeLayoutArguments(attributedString: titleText, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - leftOffset - rightInset - dateNodeLayout.size.width - 4.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(TextNodeLayoutArguments(attributedString: descriptionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 30.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let (extensionTextLayout, extensionTextApply) = extensionIconTextMakeLayout(TextNodeLayoutArguments(attributedString: extensionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 38.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
@ -511,17 +595,17 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
case let .imageRepresentation(_, representation):
|
||||
let iconSize = CGSize(width: 40.0, height: 40.0)
|
||||
let imageCorners = ImageCorners(radius: 6.0)
|
||||
let arguments = TransformImageArguments(corners: imageCorners, imageSize: representation.dimensions.cgSize.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor)
|
||||
let arguments = TransformImageArguments(corners: imageCorners, imageSize: representation.dimensions.cgSize.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.presentationData.theme.theme.list.mediaPlaceholderColor)
|
||||
iconImageApply = iconImageLayout(arguments)
|
||||
case .albumArt:
|
||||
let iconSize = CGSize(width: 40.0, height: 40.0)
|
||||
let imageCorners = ImageCorners(radius: iconSize.width / 2.0)
|
||||
let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconSize, boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor)
|
||||
let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconSize, boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.presentationData.theme.theme.list.mediaPlaceholderColor)
|
||||
iconImageApply = iconImageLayout(arguments)
|
||||
case let .roundVideo(file):
|
||||
let iconSize = CGSize(width: 40.0, height: 40.0)
|
||||
let imageCorners = ImageCorners(radius: iconSize.width / 2.0)
|
||||
let arguments = TransformImageArguments(corners: imageCorners, imageSize: (file.dimensions ?? PixelDimensions(width: 320, height: 320)).cgSize.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor)
|
||||
let arguments = TransformImageArguments(corners: imageCorners, imageSize: (file.dimensions ?? PixelDimensions(width: 320, height: 320)).cgSize.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.presentationData.theme.theme.list.mediaPlaceholderColor)
|
||||
iconImageApply = iconImageLayout(arguments)
|
||||
}
|
||||
}
|
||||
@ -532,7 +616,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
case let .imageRepresentation(file, representation):
|
||||
updateIconImageSignal = chatWebpageSnippetFile(account: item.context.account, fileReference: .message(message: MessageReference(message), media: file), representation: representation)
|
||||
case let .albumArt(file, albumArt):
|
||||
updateIconImageSignal = playerAlbumArt(postbox: item.context.account.postbox, fileReference: .message(message: MessageReference(message), media: file), albumArt: albumArt, thumbnail: true, overlayColor: UIColor(white: 0.0, alpha: 0.3), emptyColor: item.theme.list.itemAccentColor)
|
||||
updateIconImageSignal = playerAlbumArt(postbox: item.context.account.postbox, fileReference: .message(message: MessageReference(message), media: file), albumArt: albumArt, thumbnail: true, overlayColor: UIColor(white: 0.0, alpha: 0.3), emptyColor: item.presentationData.theme.theme.list.itemAccentColor)
|
||||
case let .roundVideo(file):
|
||||
updateIconImageSignal = mediaGridMessageVideo(postbox: item.context.account.postbox, videoReference: FileMediaReference.message(message: MessageReference(message), media: file), autoFetchFullSizeThumbnail: true, overlayColor: UIColor(white: 0.0, alpha: 0.3))
|
||||
}
|
||||
@ -583,9 +667,9 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
strongSelf.currentLeftOffset = leftOffset
|
||||
|
||||
if let _ = updatedTheme {
|
||||
strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor
|
||||
strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor
|
||||
strongSelf.linearProgressNode?.updateTheme(theme: item.theme)
|
||||
strongSelf.separatorNode.backgroundColor = item.presentationData.theme.theme.list.itemPlainSeparatorColor
|
||||
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.theme.list.itemHighlightedBackgroundColor
|
||||
strongSelf.linearProgressNode?.updateTheme(theme: item.presentationData.theme.theme)
|
||||
}
|
||||
|
||||
if let (selectionWidth, selectionApply) = selectionNodeWidthAndApply {
|
||||
@ -632,6 +716,10 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
transition.updateFrame(node: strongSelf.descriptionNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset + descriptionOffset, y: strongSelf.titleNode.frame.maxY + 1.0), size: descriptionNodeLayout.size))
|
||||
let _ = descriptionNodeApply()
|
||||
|
||||
let _ = dateNodeApply()
|
||||
transition.updateFrame(node: strongSelf.dateNode, frame: CGRect(origin: CGPoint(x: params.width - rightInset - dateNodeLayout.size.width, y: 11.0), size: dateNodeLayout.size))
|
||||
strongSelf.dateNode.isHidden = !item.isGlobalSearchResult
|
||||
|
||||
let iconFrame: CGRect
|
||||
if isAudio {
|
||||
let iconSize = CGSize(width: 40.0, height: 40.0)
|
||||
@ -732,8 +820,8 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
var iconStatusForegroundColor: UIColor = .white
|
||||
|
||||
if isVoice {
|
||||
iconStatusBackgroundColor = item.theme.list.itemAccentColor
|
||||
iconStatusForegroundColor = item.theme.list.itemCheckColors.foregroundColor
|
||||
iconStatusBackgroundColor = item.presentationData.theme.theme.list.itemAccentColor
|
||||
iconStatusForegroundColor = item.presentationData.theme.theme.list.itemCheckColors.foregroundColor
|
||||
}
|
||||
|
||||
if !isAudio && !isInstantVideo {
|
||||
@ -767,7 +855,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
self.iconStatusNode.transitionToState(iconStatusState)
|
||||
}
|
||||
|
||||
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
super.setHighlighted(highlighted, at: point, animated: animated)
|
||||
|
||||
if highlighted, let item = self.item, case .none = item.selection {
|
||||
@ -793,7 +881,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func transitionNode(id: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
||||
override public func transitionNode(id: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
||||
if let item = self.item, item.message.id == id, self.iconImageNode.supernode != nil {
|
||||
let iconImageNode = self.iconImageNode
|
||||
return (self.iconImageNode, self.iconImageNode.bounds, { [weak iconImageNode] in
|
||||
@ -803,15 +891,15 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
return nil
|
||||
}
|
||||
|
||||
override func updateHiddenMedia() {
|
||||
if let controllerInteraction = self.controllerInteraction, let item = self.item, controllerInteraction.hiddenMedia[item.message.id] != nil {
|
||||
override public func updateHiddenMedia() {
|
||||
if let interaction = self.interaction, let item = self.item, interaction.getHiddenMedia()[item.message.id] != nil {
|
||||
self.iconImageNode.isHidden = true
|
||||
} else {
|
||||
self.iconImageNode.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
override func updateSelectionState(animated: Bool) {
|
||||
override public func updateSelectionState(animated: Bool) {
|
||||
}
|
||||
|
||||
private func updateProgressFrame(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
@ -831,7 +919,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
switch fetchStatus {
|
||||
case let .Fetching(_, progress):
|
||||
if let file = self.currentMedia as? TelegramMediaFile, let size = file.size {
|
||||
downloadingString = "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true, decimalSeparator: item.dateTimeFormat.decimalSeparator)) / \(dataSizeString(size, forceDecimal: true, decimalSeparator: item.dateTimeFormat.decimalSeparator))"
|
||||
downloadingString = "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator)) / \(dataSizeString(size, forceDecimal: true, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator))"
|
||||
}
|
||||
descriptionOffset = 14.0
|
||||
case .Remote:
|
||||
@ -849,7 +937,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
linearProgressNode = current
|
||||
} else {
|
||||
linearProgressNode = LinearProgressNode()
|
||||
linearProgressNode.updateTheme(theme: item.theme)
|
||||
linearProgressNode.updateTheme(theme: item.presentationData.theme.theme)
|
||||
self.linearProgressNode = linearProgressNode
|
||||
self.addSubnode(linearProgressNode)
|
||||
}
|
||||
@ -859,7 +947,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
if self.downloadStatusIconNode.supernode == nil {
|
||||
self.offsetContainerNode.addSubnode(self.downloadStatusIconNode)
|
||||
}
|
||||
self.downloadStatusIconNode.image = PresentationResourcesChat.sharedMediaFileDownloadPauseIcon(item.theme)
|
||||
self.downloadStatusIconNode.image = PresentationResourcesChat.sharedMediaFileDownloadPauseIcon(item.presentationData.theme.theme)
|
||||
case .Local:
|
||||
if let linearProgressNode = self.linearProgressNode {
|
||||
self.linearProgressNode = nil
|
||||
@ -883,7 +971,7 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
if self.downloadStatusIconNode.supernode == nil {
|
||||
self.offsetContainerNode.addSubnode(self.downloadStatusIconNode)
|
||||
}
|
||||
self.downloadStatusIconNode.image = PresentationResourcesChat.sharedMediaFileDownloadStartIcon(item.theme)
|
||||
self.downloadStatusIconNode.image = PresentationResourcesChat.sharedMediaFileDownloadStartIcon(item.presentationData.theme.theme)
|
||||
}
|
||||
} else {
|
||||
if let linearProgressNode = self.linearProgressNode {
|
||||
@ -911,8 +999,8 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
self.descriptionProgressNode.isHidden = true
|
||||
self.descriptionNode.isHidden = false
|
||||
}
|
||||
let descriptionFont = Font.regular(floor(item.fontSize.baseDisplaySize * 13.0 / 17.0))
|
||||
self.descriptionProgressNode.attributedText = NSAttributedString(string: downloadingString ?? "", font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor)
|
||||
let descriptionFont = Font.regular(floor(item.presentationData.fontSize.baseDisplaySize * 13.0 / 17.0))
|
||||
self.descriptionProgressNode.attributedText = NSAttributedString(string: downloadingString ?? "", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
|
||||
let descriptionSize = self.descriptionProgressNode.updateLayout(CGSize(width: size.width - 14.0, height: size.height))
|
||||
transition.updateFrame(node: self.descriptionProgressNode, frame: CGRect(origin: self.descriptionNode.frame.origin, size: descriptionSize))
|
||||
|
||||
@ -936,8 +1024,8 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
fetch()
|
||||
}
|
||||
case .Local:
|
||||
if let item = self.item, let controllerInteraction = self.controllerInteraction {
|
||||
let _ = controllerInteraction.openMessage(item.message, .default)
|
||||
if let item = self.item, let interaction = self.interaction {
|
||||
let _ = interaction.openMessage(item.message, .default)
|
||||
}
|
||||
}
|
||||
case .playbackStatus:
|
||||
@ -948,11 +1036,11 @@ final class ListMessageFileItemNode: ListMessageNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func header() -> ListViewItemHeader? {
|
||||
override public func header() -> ListViewItemHeader? {
|
||||
return self.item?.header
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if let item = self.item, case .selectable = item.selection {
|
||||
if self.bounds.contains(point) {
|
||||
return self.view
|
||||
@ -10,51 +10,73 @@ import TelegramPresentationData
|
||||
import AccountContext
|
||||
import TelegramUIPreferences
|
||||
|
||||
final class ListMessageItem: ListViewItem {
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let fontSize: PresentationFontSize
|
||||
let dateTimeFormat: PresentationDateTimeFormat
|
||||
public final class ListMessageItemInteraction {
|
||||
let openMessage: (Message, ChatControllerInteractionOpenMessageMode) -> Bool
|
||||
let openMessageContextMenu: (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void
|
||||
let toggleMessagesSelection: ([MessageId], Bool) -> Void
|
||||
let openUrl: (String, Bool, Bool?, Message?) -> Void
|
||||
let openInstantPage: (Message, ChatMessageItemAssociatedData?) -> Void
|
||||
let longTap: (ChatControllerInteractionLongTapAction, Message?) -> Void
|
||||
let getHiddenMedia: () -> [MessageId: [Media]]
|
||||
|
||||
public init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?) -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, openUrl: @escaping (String, Bool, Bool?, Message?) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, getHiddenMedia: @escaping () -> [MessageId: [Media]]) {
|
||||
self.openMessage = openMessage
|
||||
self.openMessageContextMenu = openMessageContextMenu
|
||||
self.toggleMessagesSelection = toggleMessagesSelection
|
||||
self.openUrl = openUrl
|
||||
self.openInstantPage = openInstantPage
|
||||
self.longTap = longTap
|
||||
self.getHiddenMedia = getHiddenMedia
|
||||
}
|
||||
}
|
||||
|
||||
public final class ListMessageItem: ListViewItem {
|
||||
let presentationData: ChatPresentationData
|
||||
let context: AccountContext
|
||||
let chatLocation: ChatLocation
|
||||
let controllerInteraction: ChatControllerInteraction
|
||||
let interaction: ListMessageItemInteraction
|
||||
let message: Message
|
||||
let selection: ChatHistoryMessageSelection
|
||||
let hintIsLink: Bool
|
||||
let isGlobalSearchResult: Bool
|
||||
|
||||
let header: ListMessageDateHeader?
|
||||
let header: ListViewItemHeader?
|
||||
|
||||
let selectable: Bool = true
|
||||
public let selectable: Bool = true
|
||||
|
||||
public init(theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, dateTimeFormat: PresentationDateTimeFormat, context: AccountContext, chatLocation: ChatLocation, controllerInteraction: ChatControllerInteraction, message: Message, selection: ChatHistoryMessageSelection, displayHeader: Bool) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.fontSize = fontSize
|
||||
self.dateTimeFormat = dateTimeFormat
|
||||
public init(presentationData: ChatPresentationData, context: AccountContext, chatLocation: ChatLocation, interaction: ListMessageItemInteraction, message: Message, selection: ChatHistoryMessageSelection, displayHeader: Bool, customHeader: ListViewItemHeader? = nil, hintIsLink: Bool = false, isGlobalSearchResult: Bool = false) {
|
||||
self.presentationData = presentationData
|
||||
self.context = context
|
||||
self.chatLocation = chatLocation
|
||||
self.controllerInteraction = controllerInteraction
|
||||
self.interaction = interaction
|
||||
self.message = message
|
||||
if displayHeader {
|
||||
self.header = ListMessageDateHeader(timestamp: message.timestamp, theme: theme, strings: strings, fontSize: fontSize)
|
||||
if let header = customHeader {
|
||||
self.header = header
|
||||
} else if displayHeader {
|
||||
self.header = ListMessageDateHeader(timestamp: message.timestamp, theme: presentationData.theme.theme, strings: presentationData.strings, fontSize: presentationData.fontSize)
|
||||
} else {
|
||||
self.header = nil
|
||||
}
|
||||
self.selection = selection
|
||||
self.hintIsLink = hintIsLink
|
||||
self.isGlobalSearchResult = isGlobalSearchResult
|
||||
}
|
||||
|
||||
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
var viewClassName: AnyClass = ListMessageSnippetItemNode.self
|
||||
|
||||
for media in message.media {
|
||||
if let _ = media as? TelegramMediaFile {
|
||||
viewClassName = ListMessageFileItemNode.self
|
||||
break
|
||||
if !self.hintIsLink {
|
||||
for media in self.message.media {
|
||||
if let _ = media as? TelegramMediaFile {
|
||||
viewClassName = ListMessageFileItemNode.self
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let configure = { () -> Void in
|
||||
let node = (viewClassName as! ListMessageNode.Type).init()
|
||||
node.controllerInteraction = self.controllerInteraction
|
||||
node.interaction = self.interaction
|
||||
node.setupItem(self)
|
||||
|
||||
let nodeLayout = node.asyncLayout()
|
||||
@ -106,11 +128,11 @@ final class ListMessageItem: ListViewItem {
|
||||
}
|
||||
}
|
||||
|
||||
func selected(listView: ListView) {
|
||||
public func selected(listView: ListView) {
|
||||
listView.clearHighlightAnimated(true)
|
||||
|
||||
if case let .selectable(selected) = self.selection {
|
||||
self.controllerInteraction.toggleMessagesSelection([self.message.id], !selected)
|
||||
self.interaction.toggleMessagesSelection([self.message.id], !selected)
|
||||
} else {
|
||||
listView.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? ListMessageFileItemNode {
|
||||
@ -3,10 +3,11 @@ import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import AccountContext
|
||||
|
||||
class ListMessageNode: ListViewItemNode {
|
||||
public class ListMessageNode: ListViewItemNode {
|
||||
var item: ListMessageItem?
|
||||
var controllerInteraction: ChatControllerInteraction?
|
||||
var interaction: ListMessageItemInteraction?
|
||||
|
||||
required init() {
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
@ -19,7 +20,7 @@ class ListMessageNode: ListViewItemNode {
|
||||
override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: ListMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
|
||||
public func asyncLayout() -> (_ item: ListMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
|
||||
return { _, params, _, _, _ in
|
||||
return (ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 1.0), insets: UIEdgeInsets()), { _ in
|
||||
|
||||
@ -27,13 +28,13 @@ class ListMessageNode: ListViewItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
func transitionNode(id: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
||||
public func transitionNode(id: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateHiddenMedia() {
|
||||
public func updateHiddenMedia() {
|
||||
}
|
||||
|
||||
func updateSelectionState(animated: Bool) {
|
||||
public func updateSelectionState(animated: Bool) {
|
||||
}
|
||||
}
|
||||
@ -13,12 +13,15 @@ import TextFormat
|
||||
import PhotoResources
|
||||
import WebsiteType
|
||||
import UrlHandling
|
||||
import UrlWhitelist
|
||||
import AccountContext
|
||||
import TelegramStringFormatting
|
||||
|
||||
private let iconFont = Font.with(size: 30.0, design: .round, traits: [.bold])
|
||||
|
||||
private let iconTextBackgroundImage = generateStretchableFilledCircleImage(radius: 6.0, color: UIColor(rgb: 0xFF9500))
|
||||
|
||||
final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
public final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
private let contextSourceNode: ContextExtractedContentContainingNode
|
||||
private let containerNode: ContextControllerSourceNode
|
||||
private let extractedBackgroundImageNode: ASImageNode
|
||||
@ -34,9 +37,11 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
|
||||
private let titleNode: TextNode
|
||||
private let descriptionNode: TextNode
|
||||
private let dateNode: TextNode
|
||||
private let instantViewIconNode: ASImageNode
|
||||
private let linkNode: TextNode
|
||||
private var linkHighlightingNode: LinkHighlightingNode?
|
||||
private let authorNode: TextNode
|
||||
|
||||
private let iconTextBackgroundNode: ASImageNode
|
||||
private let iconTextNode: TextNode
|
||||
@ -44,7 +49,7 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
|
||||
private var currentIconImageRepresentation: TelegramMediaImageRepresentation?
|
||||
private var currentMedia: Media?
|
||||
var currentPrimaryUrl: String?
|
||||
public var currentPrimaryUrl: String?
|
||||
private var currentIsInstantView: Bool?
|
||||
|
||||
private var appliedItem: ListMessageItem?
|
||||
@ -72,6 +77,9 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
self.descriptionNode = TextNode()
|
||||
self.descriptionNode.isUserInteractionEnabled = false
|
||||
|
||||
self.dateNode = TextNode()
|
||||
self.dateNode.isUserInteractionEnabled = false
|
||||
|
||||
self.instantViewIconNode = ASImageNode()
|
||||
self.instantViewIconNode.isLayerBacked = true
|
||||
self.instantViewIconNode.displaysAsynchronously = false
|
||||
@ -90,6 +98,9 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
self.iconImageNode = TransformImageNode()
|
||||
self.iconImageNode.displaysAsynchronously = false
|
||||
|
||||
self.authorNode = TextNode()
|
||||
self.authorNode.isUserInteractionEnabled = false
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.separatorNode)
|
||||
@ -102,16 +113,18 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
self.contextSourceNode.contentNode.addSubnode(self.offsetContainerNode)
|
||||
self.offsetContainerNode.addSubnode(self.titleNode)
|
||||
self.offsetContainerNode.addSubnode(self.descriptionNode)
|
||||
self.offsetContainerNode.addSubnode(self.dateNode)
|
||||
self.offsetContainerNode.addSubnode(self.linkNode)
|
||||
self.offsetContainerNode.addSubnode(self.instantViewIconNode)
|
||||
self.offsetContainerNode.addSubnode(self.iconImageNode)
|
||||
self.offsetContainerNode.addSubnode(self.authorNode)
|
||||
|
||||
self.containerNode.activated = { [weak self] gesture, _ in
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
return
|
||||
}
|
||||
|
||||
item.controllerInteraction.openMessageContextMenu(item.message, false, strongSelf.contextSourceNode, strongSelf.contextSourceNode.bounds, gesture)
|
||||
item.interaction.openMessageContextMenu(item.message, false, strongSelf.contextSourceNode, strongSelf.contextSourceNode.bounds, gesture)
|
||||
}
|
||||
|
||||
self.contextSourceNode.willUpdateIsExtractedToContextPreview = { [weak self] isExtracted, transition in
|
||||
@ -120,7 +133,7 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
}
|
||||
|
||||
if isExtracted {
|
||||
strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: item.theme.list.plainBackgroundColor)
|
||||
strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: item.presentationData.theme.theme.list.plainBackgroundColor)
|
||||
}
|
||||
|
||||
if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect {
|
||||
@ -135,6 +148,7 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
self?.extractedBackgroundImageNode.image = nil
|
||||
}
|
||||
})
|
||||
transition.updateAlpha(node: strongSelf.dateNode, alpha: isExtracted ? 0.0 : 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
@ -142,7 +156,7 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
override public func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
|
||||
@ -182,17 +196,19 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
self.addTransitionOffsetAnimation(0.0, duration: duration, beginAt: currentTimestamp)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
override func asyncLayout() -> (_ item: ListMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
|
||||
override public func asyncLayout() -> (_ item: ListMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
|
||||
let titleNodeMakeLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let descriptionNodeMakeLayout = TextNode.asyncLayout(self.descriptionNode)
|
||||
let linkNodeMakeLayout = TextNode.asyncLayout(self.linkNode)
|
||||
let dateNodeMakeLayout = TextNode.asyncLayout(self.dateNode)
|
||||
let iconTextMakeLayout = TextNode.asyncLayout(self.iconTextNode)
|
||||
let iconImageLayout = self.iconImageNode.asyncLayout()
|
||||
|
||||
let authorNodeMakeLayout = TextNode.asyncLayout(self.authorNode)
|
||||
|
||||
let currentIconImageRepresentation = self.currentIconImageRepresentation
|
||||
|
||||
let currentItem = self.appliedItem
|
||||
@ -202,19 +218,21 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
return { [weak self] item, params, _, _, dateHeaderAtBottom in
|
||||
var updatedTheme: PresentationTheme?
|
||||
|
||||
if currentItem?.theme !== item.theme {
|
||||
updatedTheme = item.theme
|
||||
if currentItem?.presentationData.theme.theme !== item.presentationData.theme.theme {
|
||||
updatedTheme = item.presentationData.theme.theme
|
||||
}
|
||||
|
||||
let titleFont = Font.semibold(floor(item.fontSize.baseDisplaySize * 16.0 / 17.0))
|
||||
let descriptionFont = Font.regular(floor(item.fontSize.baseDisplaySize * 14.0 / 17.0))
|
||||
let titleFont = Font.semibold(floor(item.presentationData.fontSize.baseDisplaySize * 16.0 / 17.0))
|
||||
let descriptionFont = Font.regular(floor(item.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0))
|
||||
let dateFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0))
|
||||
let authorFont = Font.regular(floor(item.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0))
|
||||
|
||||
let leftInset: CGFloat = 65.0 + params.leftInset
|
||||
|
||||
var leftOffset: CGFloat = 0.0
|
||||
var selectionNodeWidthAndApply: (CGFloat, (CGSize, Bool) -> ItemListSelectableControlNode)?
|
||||
if case let .selectable(selected) = item.selection {
|
||||
let (selectionWidth, selectionApply) = selectionNodeLayout(item.theme.list.itemCheckColors.strokeColor, item.theme.list.itemCheckColors.fillColor, item.theme.list.itemCheckColors.foregroundColor, selected, false)
|
||||
let (selectionWidth, selectionApply) = selectionNodeLayout(item.presentationData.theme.theme.list.itemCheckColors.strokeColor, item.presentationData.theme.theme.list.itemCheckColors.fillColor, item.presentationData.theme.theme.list.itemCheckColors.foregroundColor, selected, false)
|
||||
selectionNodeWidthAndApply = (selectionWidth, selectionApply)
|
||||
leftOffset += selectionWidth
|
||||
}
|
||||
@ -255,7 +273,7 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
iconText = NSAttributedString(string: host[..<host.index(after: host.startIndex)].uppercased(), font: iconFont, textColor: UIColor.white)
|
||||
}
|
||||
|
||||
title = NSAttributedString(string: content.title ?? content.websiteName ?? hostName, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor)
|
||||
title = NSAttributedString(string: content.title ?? content.websiteName ?? hostName, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
|
||||
|
||||
if let image = content.image {
|
||||
if let representation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 80, height: 80)) {
|
||||
@ -268,11 +286,11 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
}
|
||||
|
||||
let mutableDescriptionText = NSMutableAttributedString()
|
||||
if let text = content.text {
|
||||
mutableDescriptionText.append(NSAttributedString(string: text + "\n", font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor))
|
||||
if let text = content.text, !item.isGlobalSearchResult {
|
||||
mutableDescriptionText.append(NSAttributedString(string: text + "\n", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor))
|
||||
}
|
||||
|
||||
let plainUrlString = NSAttributedString(string: content.displayUrl, font: descriptionFont, textColor: item.theme.list.itemAccentColor)
|
||||
let plainUrlString = NSAttributedString(string: content.displayUrl, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemAccentColor)
|
||||
let urlString = NSMutableAttributedString()
|
||||
urlString.append(plainUrlString)
|
||||
urlString.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.URL), value: content.displayUrl, range: NSMakeRange(0, urlString.length))
|
||||
@ -294,6 +312,21 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
}
|
||||
}
|
||||
|
||||
for media in item.message.media {
|
||||
if let image = media as? TelegramMediaImage {
|
||||
if let representation = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 80, height: 80)) {
|
||||
iconImageReferenceAndRepresentation = (.message(message: MessageReference(item.message), media: image), representation)
|
||||
}
|
||||
break
|
||||
}
|
||||
if let file = media as? TelegramMediaFile {
|
||||
if let representation = smallestImageRepresentation(file.previewRepresentations) {
|
||||
iconImageReferenceAndRepresentation = (.message(message: MessageReference(item.message), media: file), representation)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var entities: [MessageTextEntity]?
|
||||
|
||||
entities = messageEntities
|
||||
@ -328,25 +361,25 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
if let url = parsedUrl, let host = host {
|
||||
primaryUrl = urlString
|
||||
if url.path.hasPrefix("/addstickers/") {
|
||||
title = NSAttributedString(string: urlString, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor)
|
||||
title = NSAttributedString(string: urlString, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
|
||||
|
||||
iconText = NSAttributedString(string: "S", font: iconFont, textColor: UIColor.white)
|
||||
} else {
|
||||
iconText = NSAttributedString(string: host[..<host.index(after: host.startIndex)].uppercased(), font: iconFont, textColor: UIColor.white)
|
||||
|
||||
title = NSAttributedString(string: host, font: titleFont, textColor: item.theme.list.itemPrimaryTextColor)
|
||||
title = NSAttributedString(string: host, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
|
||||
}
|
||||
let mutableDescriptionText = NSMutableAttributedString()
|
||||
|
||||
let (messageTextUrl, _) = parseUrl(url: item.message.text, wasConcealed: false)
|
||||
|
||||
if messageTextUrl != rawUrlString {
|
||||
mutableDescriptionText.append(NSAttributedString(string: item.message.text + "\n", font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor))
|
||||
if messageTextUrl != rawUrlString, !item.isGlobalSearchResult {
|
||||
mutableDescriptionText.append(NSAttributedString(string: item.message.text + "\n", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor))
|
||||
}
|
||||
|
||||
let urlAttributedString = NSMutableAttributedString()
|
||||
urlAttributedString.append(NSAttributedString(string: urlString, font: descriptionFont, textColor: item.theme.list.itemAccentColor))
|
||||
if item.theme.list.itemAccentColor.isEqual(item.theme.list.itemPrimaryTextColor) {
|
||||
urlAttributedString.append(NSAttributedString(string: urlString, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemAccentColor))
|
||||
if item.presentationData.theme.theme.list.itemAccentColor.isEqual(item.presentationData.theme.theme.list.itemPrimaryTextColor) {
|
||||
urlAttributedString.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: NSMakeRange(0, urlAttributedString.length))
|
||||
}
|
||||
urlAttributedString.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.URL), value: urlString, range: NSMakeRange(0, urlAttributedString.length))
|
||||
@ -355,6 +388,50 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
descriptionText = mutableDescriptionText
|
||||
}
|
||||
break loop
|
||||
case let .TextUrl(url):
|
||||
var range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
|
||||
let nsString = item.message.text as NSString
|
||||
if range.location + range.length > nsString.length {
|
||||
range.location = max(0, nsString.length - range.length)
|
||||
range.length = nsString.length - range.location
|
||||
}
|
||||
let tempTitleString = (nsString.substring(with: range) as String).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
var (urlString, concealed) = parseUrl(url: url, wasConcealed: false)
|
||||
let rawUrlString = urlString
|
||||
var parsedUrl = URL(string: urlString)
|
||||
if parsedUrl == nil || parsedUrl!.host == nil || parsedUrl!.host!.isEmpty {
|
||||
urlString = "http://" + urlString
|
||||
parsedUrl = URL(string: urlString)
|
||||
}
|
||||
let host: String? = concealed ? urlString : parsedUrl?.host
|
||||
if let url = parsedUrl, let host = host {
|
||||
primaryUrl = urlString
|
||||
title = NSAttributedString(string: tempTitleString as String, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor)
|
||||
if url.path.hasPrefix("/addstickers/") {
|
||||
iconText = NSAttributedString(string: "S", font: iconFont, textColor: UIColor.white)
|
||||
} else {
|
||||
iconText = NSAttributedString(string: host[..<host.index(after: host.startIndex)].uppercased(), font: iconFont, textColor: UIColor.white)
|
||||
}
|
||||
let mutableDescriptionText = NSMutableAttributedString()
|
||||
|
||||
let (messageTextUrl, _) = parseUrl(url: item.message.text, wasConcealed: false)
|
||||
|
||||
if messageTextUrl != rawUrlString, !item.isGlobalSearchResult {
|
||||
mutableDescriptionText.append(NSAttributedString(string: item.message.text + "\n", font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor))
|
||||
}
|
||||
|
||||
let urlAttributedString = NSMutableAttributedString()
|
||||
urlAttributedString.append(NSAttributedString(string: urlString, font: descriptionFont, textColor: item.presentationData.theme.theme.list.itemAccentColor))
|
||||
if item.presentationData.theme.theme.list.itemAccentColor.isEqual(item.presentationData.theme.theme.list.itemPrimaryTextColor) {
|
||||
urlAttributedString.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue as NSNumber, range: NSMakeRange(0, urlAttributedString.length))
|
||||
}
|
||||
urlAttributedString.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.URL), value: urlString, range: NSMakeRange(0, urlAttributedString.length))
|
||||
linkText = urlAttributedString
|
||||
|
||||
descriptionText = mutableDescriptionText
|
||||
}
|
||||
break loop
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -362,14 +439,20 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
}
|
||||
}
|
||||
|
||||
let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(TextNodeLayoutArguments(attributedString: title, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - params.rightInset - 16.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
|
||||
let dateText = stringForRelativeTimestamp(strings: item.presentationData.strings, relativeTimestamp: item.message.timestamp, relativeTo: timestamp, dateTimeFormat: item.presentationData.dateTimeFormat)
|
||||
let dateAttributedString = NSAttributedString(string: dateText, font: dateFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
|
||||
|
||||
let (dateNodeLayout, dateNodeApply) = dateNodeMakeLayout(TextNodeLayoutArguments(attributedString: dateAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - params.rightInset - 12.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(TextNodeLayoutArguments(attributedString: title, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .middle, constrainedSize: CGSize(width: params.width - leftInset - leftOffset - 8.0 - params.rightInset - 16.0 - dateNodeLayout.size.width, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(TextNodeLayoutArguments(attributedString: descriptionText, backgroundColor: nil, maximumNumberOfLines: 3, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - params.rightInset - 16.0 - 8.0, height: CGFloat.infinity), alignment: .natural, lineSpacing: 0.3, cutout: nil, insets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)))
|
||||
|
||||
let (linkNodeLayout, linkNodeApply) = linkNodeMakeLayout(TextNodeLayoutArguments(attributedString: linkText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - params.rightInset - 16.0 - 8.0, height: CGFloat.infinity), alignment: .natural, lineSpacing: 0.3, cutout: isInstantView ? TextNodeCutout(topLeft: CGSize(width: 14.0, height: 8.0)) : nil, insets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0)))
|
||||
var instantViewImage: UIImage?
|
||||
if isInstantView {
|
||||
instantViewImage = PresentationResourcesChat.sharedMediaInstantViewIcon(item.theme)
|
||||
instantViewImage = PresentationResourcesChat.sharedMediaInstantViewIcon(item.presentationData.theme.theme)
|
||||
}
|
||||
|
||||
let (iconTextLayout, iconTextApply) = iconTextMakeLayout(TextNodeLayoutArguments(attributedString: iconText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 38.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
@ -378,7 +461,7 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
if let iconImageReferenceAndRepresentation = iconImageReferenceAndRepresentation {
|
||||
let iconSize = CGSize(width: 40.0, height: 40.0)
|
||||
let imageCorners = ImageCorners(radius: 6.0)
|
||||
let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconImageReferenceAndRepresentation.1.dimensions.cgSize.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor)
|
||||
let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconImageReferenceAndRepresentation.1.dimensions.cgSize.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: UIEdgeInsets(), emptyColor: item.presentationData.theme.theme.list.mediaPlaceholderColor)
|
||||
iconImageApply = iconImageLayout(arguments)
|
||||
}
|
||||
|
||||
@ -394,7 +477,19 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
}
|
||||
}
|
||||
|
||||
let contentHeight = 9.0 + titleNodeLayout.size.height + 10.0 + descriptionNodeLayout.size.height + linkNodeLayout.size.height
|
||||
var authorString = ""
|
||||
if item.isGlobalSearchResult {
|
||||
authorString = fullAuthorString(for: item)
|
||||
}
|
||||
|
||||
let authorText = NSAttributedString(string: authorString, font: authorFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor)
|
||||
|
||||
let (authorNodeLayout, authorNodeApply) = authorNodeMakeLayout(TextNodeLayoutArguments(attributedString: authorText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - params.rightInset - 30.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
var contentHeight = 9.0 + titleNodeLayout.size.height + 10.0 + descriptionNodeLayout.size.height + linkNodeLayout.size.height
|
||||
if item.isGlobalSearchResult {
|
||||
contentHeight += authorNodeLayout.size.height
|
||||
}
|
||||
|
||||
var insets = UIEdgeInsets()
|
||||
if dateHeaderAtBottom, let header = item.header {
|
||||
@ -434,8 +529,8 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
strongSelf.currentIsInstantView = isInstantView
|
||||
|
||||
if let _ = updatedTheme {
|
||||
strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor
|
||||
strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor
|
||||
strongSelf.separatorNode.backgroundColor = item.presentationData.theme.theme.list.itemPlainSeparatorColor
|
||||
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.theme.list.itemHighlightedBackgroundColor
|
||||
}
|
||||
|
||||
if let (selectionWidth, selectionApply) = selectionNodeWidthAndApply {
|
||||
@ -468,10 +563,18 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
transition.updateFrame(node: strongSelf.descriptionNode, frame: descriptionFrame)
|
||||
let _ = descriptionNodeApply()
|
||||
|
||||
let _ = dateNodeApply()
|
||||
transition.updateFrame(node: strongSelf.dateNode, frame: CGRect(origin: CGPoint(x: params.width - params.rightInset - dateNodeLayout.size.width - 8.0, y: 11.0), size: dateNodeLayout.size))
|
||||
strongSelf.dateNode.isHidden = !item.isGlobalSearchResult
|
||||
|
||||
let linkFrame = CGRect(origin: CGPoint(x: leftOffset + leftInset - 1.0, y: descriptionFrame.maxY), size: linkNodeLayout.size)
|
||||
transition.updateFrame(node: strongSelf.linkNode, frame: linkFrame)
|
||||
let _ = linkNodeApply()
|
||||
|
||||
let _ = authorNodeApply()
|
||||
transition.updateFrame(node: strongSelf.authorNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset, y: linkFrame.maxY + 1.0), size: authorNodeLayout.size))
|
||||
strongSelf.authorNode.isHidden = !item.isGlobalSearchResult
|
||||
|
||||
if let image = instantViewImage {
|
||||
strongSelf.instantViewIconNode.image = image
|
||||
transition.updateFrame(node: strongSelf.instantViewIconNode, frame: CGRect(origin: linkFrame.origin.offsetBy(dx: 0.0, dy: 4.0), size: image.size))
|
||||
@ -528,7 +631,7 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
override public func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
super.setHighlighted(highlighted, at: point, animated: animated)
|
||||
|
||||
if highlighted, let item = self.item, case .none = item.selection, self.urlAtPoint(point) == nil {
|
||||
@ -554,7 +657,7 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func transitionNode(id: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
||||
override public func transitionNode(id: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
||||
if let item = self.item, item.message.id == id, self.iconImageNode.supernode != nil {
|
||||
let iconImageNode = self.iconImageNode
|
||||
return (self.iconImageNode, self.iconImageNode.bounds, { [weak iconImageNode] in
|
||||
@ -564,15 +667,15 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
return nil
|
||||
}
|
||||
|
||||
override func updateHiddenMedia() {
|
||||
if let controllerInteraction = self.controllerInteraction, let item = self.item, controllerInteraction.hiddenMedia[item.message.id] != nil {
|
||||
override public func updateHiddenMedia() {
|
||||
if let interaction = self.interaction, let item = self.item, interaction.getHiddenMedia()[item.message.id] != nil {
|
||||
self.iconImageNode.isHidden = true
|
||||
} else {
|
||||
self.iconImageNode.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
override func updateSelectionState(animated: Bool) {
|
||||
override public func updateSelectionState(animated: Bool) {
|
||||
}
|
||||
|
||||
func activateMedia() {
|
||||
@ -580,30 +683,28 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
if let webpage = self.currentMedia as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
|
||||
if content.instantPage != nil {
|
||||
if websiteType(of: content.websiteName) == .instagram {
|
||||
if !item.controllerInteraction.openMessage(item.message, .default) {
|
||||
item.controllerInteraction.openInstantPage(item.message, nil)
|
||||
if !item.interaction.openMessage(item.message, .default) {
|
||||
item.interaction.openInstantPage(item.message, nil)
|
||||
}
|
||||
} else {
|
||||
item.controllerInteraction.openInstantPage(item.message, nil)
|
||||
item.interaction.openInstantPage(item.message, nil)
|
||||
}
|
||||
} else {
|
||||
if isTelegramMeLink(content.url) || !item.controllerInteraction.openMessage(item.message, .link) {
|
||||
item.controllerInteraction.openUrl(currentPrimaryUrl, false, false, nil)
|
||||
if isTelegramMeLink(content.url) || !item.interaction.openMessage(item.message, .link) {
|
||||
item.interaction.openUrl(currentPrimaryUrl, false, false, nil)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !item.controllerInteraction.openMessage(item.message, .default) {
|
||||
item.controllerInteraction.openUrl(currentPrimaryUrl, false, false, nil)
|
||||
}
|
||||
item.interaction.openUrl(currentPrimaryUrl, false, false, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func header() -> ListViewItemHeader? {
|
||||
override public func header() -> ListViewItemHeader? {
|
||||
return self.item?.header
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if let item = self.item, case .selectable = item.selection {
|
||||
if self.bounds.contains(point) {
|
||||
return self.view
|
||||
@ -640,13 +741,13 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
case .tap, .longTap:
|
||||
if let item = self.item, let url = self.urlAtPoint(location) {
|
||||
if case .longTap = gesture {
|
||||
item.controllerInteraction.longTap(ChatControllerInteractionLongTapAction.url(url), item.message)
|
||||
item.interaction.longTap(ChatControllerInteractionLongTapAction.url(url), item.message)
|
||||
} else if url == self.currentPrimaryUrl {
|
||||
if !item.controllerInteraction.openMessage(item.message, .default) {
|
||||
item.controllerInteraction.openUrl(url, false, false, nil)
|
||||
if !item.interaction.openMessage(item.message, .default) {
|
||||
item.interaction.openUrl(url, false, false, nil)
|
||||
}
|
||||
} else {
|
||||
item.controllerInteraction.openUrl(url, false, true, nil)
|
||||
item.interaction.openUrl(url, false, true, nil)
|
||||
}
|
||||
}
|
||||
case .hold, .doubleTap:
|
||||
@ -683,7 +784,7 @@ final class ListMessageSnippetItemNode: ListMessageNode {
|
||||
if let current = self.linkHighlightingNode {
|
||||
linkHighlightingNode = current
|
||||
} else {
|
||||
linkHighlightingNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.theme.chat.message.incoming.linkHighlightColor : item.theme.chat.message.outgoing.linkHighlightColor)
|
||||
linkHighlightingNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.linkHighlightColor : item.presentationData.theme.theme.chat.message.outgoing.linkHighlightColor)
|
||||
self.linkHighlightingNode = linkHighlightingNode
|
||||
self.offsetContainerNode.insertSubnode(linkHighlightingNode, belowSubnode: self.linkNode)
|
||||
}
|
||||
@ -250,7 +250,7 @@ public final class LiveLocationManagerImpl: LiveLocationManager {
|
||||
updatedMedia[i] = TelegramMediaMap(latitude: media.latitude, longitude: media.longitude, geoPlace: media.geoPlace, venue: media.venue, liveBroadcastingTimeout: max(0, timestamp - currentMessage.timestamp - 1))
|
||||
}
|
||||
}
|
||||
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: updatedMedia))
|
||||
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: updatedMedia))
|
||||
})
|
||||
}
|
||||
}).start()
|
||||
|
||||
@ -10,6 +10,7 @@ static_library(
|
||||
"//submodules/SyncCore:SyncCore#shared",
|
||||
"//submodules/Postbox:Postbox#shared",
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit#shared",
|
||||
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
|
||||
],
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||
|
||||
@ -11,6 +11,7 @@ swift_library(
|
||||
"//submodules/SyncCore:SyncCore",
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -3,10 +3,10 @@ load("//Config:buck_rule_macros.bzl", "static_library", "framework", "glob_map",
|
||||
framework(
|
||||
name = "MtProtoKit",
|
||||
srcs = glob([
|
||||
"Sources/*.m",
|
||||
"Sources/**/*.m",
|
||||
]),
|
||||
headers = glob([
|
||||
"Sources/*.h",
|
||||
"Sources/**/*.h",
|
||||
]),
|
||||
exported_headers = glob([
|
||||
"PublicHeaders/**/*.h",
|
||||
|
||||
@ -4,8 +4,8 @@ objc_library(
|
||||
enable_modules = True,
|
||||
module_name = "MtProtoKit",
|
||||
srcs = glob([
|
||||
"Sources/*.m",
|
||||
"Sources/*.h",
|
||||
"Sources/**/*.m",
|
||||
"Sources/**/*.h",
|
||||
]),
|
||||
hdrs = glob([
|
||||
"PublicHeaders/**/*.h",
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <MtProtoKit/MTMessageService.h>
|
||||
#import <MtProtoKit/MTDatacenterAuthInfo.h>
|
||||
|
||||
@interface MTBindKeyMessageService : NSObject <MTMessageService>
|
||||
|
||||
- (instancetype)initWithPersistentKey:(MTDatacenterAuthKey *)persistentKey ephemeralKey:(MTDatacenterAuthKey *)ephemeralKey completion:(void (^)(bool))completion;
|
||||
|
||||
@end
|
||||
@ -20,7 +20,7 @@
|
||||
@optional
|
||||
|
||||
- (void)contextDatacenterAddressSetUpdated:(MTContext *)context datacenterId:(NSInteger)datacenterId addressSet:(MTDatacenterAddressSet *)addressSet;
|
||||
- (void)contextDatacenterAuthInfoUpdated:(MTContext *)context datacenterId:(NSInteger)datacenterId authInfo:(MTDatacenterAuthInfo *)authInfo;
|
||||
- (void)contextDatacenterAuthInfoUpdated:(MTContext *)context datacenterId:(NSInteger)datacenterId authInfo:(MTDatacenterAuthInfo *)authInfo selector:(MTDatacenterAuthInfoSelector)selector;
|
||||
- (void)contextDatacenterAuthTokenUpdated:(MTContext *)context datacenterId:(NSInteger)datacenterId authToken:(id)authToken;
|
||||
- (void)contextDatacenterTransportSchemesUpdated:(MTContext *)context datacenterId:(NSInteger)datacenterId shouldReset:(bool)shouldReset;
|
||||
- (void)contextIsPasswordRequiredUpdated:(MTContext *)context datacenterId:(NSInteger)datacenterId;
|
||||
@ -49,6 +49,7 @@
|
||||
@property (nonatomic, strong, readonly) MTApiEnvironment *apiEnvironment;
|
||||
@property (nonatomic, readonly) bool isTestingEnvironment;
|
||||
@property (nonatomic, readonly) bool useTempAuthKeys;
|
||||
@property (nonatomic) int32_t tempKeyExpiration;
|
||||
|
||||
+ (int32_t)fixedTimeDifference;
|
||||
+ (void)setFixedTimeDifference:(int32_t)fixedTimeDifference;
|
||||
@ -73,7 +74,7 @@
|
||||
- (void)updateAddressSetForDatacenterWithId:(NSInteger)datacenterId addressSet:(MTDatacenterAddressSet *)addressSet forceUpdateSchemes:(bool)forceUpdateSchemes;
|
||||
- (void)addAddressForDatacenterWithId:(NSInteger)datacenterId address:(MTDatacenterAddress *)address;
|
||||
- (void)updateTransportSchemeForDatacenterWithId:(NSInteger)datacenterId transportScheme:(MTTransportScheme *)transportScheme media:(bool)media isProxy:(bool)isProxy;
|
||||
- (void)updateAuthInfoForDatacenterWithId:(NSInteger)datacenterId authInfo:(MTDatacenterAuthInfo *)authInfo;
|
||||
- (void)updateAuthInfoForDatacenterWithId:(NSInteger)datacenterId authInfo:(MTDatacenterAuthInfo *)authInfo selector:(MTDatacenterAuthInfoSelector)selector;
|
||||
|
||||
- (bool)isPasswordInputRequiredForDatacenterWithId:(NSInteger)datacenterId;
|
||||
- (bool)updatePasswordInputRequiredForDatacenterWithId:(NSInteger)datacenterId required:(bool)required;
|
||||
@ -95,7 +96,7 @@
|
||||
- (void)transportSchemeForDatacenterWithIdRequired:(NSInteger)datacenterId media:(bool)media;
|
||||
- (void)invalidateTransportSchemeForDatacenterId:(NSInteger)datacenterId transportScheme:(MTTransportScheme *)transportScheme isProbablyHttp:(bool)isProbablyHttp media:(bool)media;
|
||||
- (void)revalidateTransportSchemeForDatacenterId:(NSInteger)datacenterId transportScheme:(MTTransportScheme *)transportScheme media:(bool)media;
|
||||
- (MTDatacenterAuthInfo *)authInfoForDatacenterWithId:(NSInteger)datacenterId;
|
||||
- (MTDatacenterAuthInfo *)authInfoForDatacenterWithId:(NSInteger)datacenterId selector:(MTDatacenterAuthInfoSelector)selector;
|
||||
|
||||
- (NSArray<NSDictionary *> *)publicKeysForDatacenterWithId:(NSInteger)datacenterId;
|
||||
- (void)updatePublicKeysForDatacenterWithId:(NSInteger)datacenterId publicKeys:(NSArray<NSDictionary *> *)publicKeys;
|
||||
@ -107,8 +108,7 @@
|
||||
- (void)updateAuthTokenForDatacenterWithId:(NSInteger)datacenterId authToken:(id)authToken;
|
||||
|
||||
- (void)addressSetForDatacenterWithIdRequired:(NSInteger)datacenterId;
|
||||
- (void)authInfoForDatacenterWithIdRequired:(NSInteger)datacenterId isCdn:(bool)isCdn;
|
||||
- (void)tempAuthKeyForDatacenterWithIdRequired:(NSInteger)datacenterId keyType:(MTDatacenterAuthTempKeyType)keyType;
|
||||
- (void)authInfoForDatacenterWithIdRequired:(NSInteger)datacenterId isCdn:(bool)isCdn selector:(MTDatacenterAuthInfoSelector)selector;
|
||||
- (void)authTokenForDatacenterWithIdRequired:(NSInteger)datacenterId authToken:(id)authToken masterDatacenterId:(NSInteger)masterDatacenterId;
|
||||
|
||||
- (void)reportProblemsWithDatacenterAddressForId:(NSInteger)datacenterId address:(MTDatacenterAddress *)address;
|
||||
|
||||
@ -4,23 +4,12 @@
|
||||
|
||||
|
||||
@class MTContext;
|
||||
@class MTDatacenterAuthAction;
|
||||
|
||||
@protocol MTDatacenterAuthActionDelegate <NSObject>
|
||||
|
||||
- (void)datacenterAuthActionCompleted:(MTDatacenterAuthAction *)action;
|
||||
|
||||
@end
|
||||
|
||||
@interface MTDatacenterAuthAction : NSObject
|
||||
|
||||
@property (nonatomic, readonly) bool tempAuth;
|
||||
@property (nonatomic, weak) id<MTDatacenterAuthActionDelegate> delegate;
|
||||
@property (nonatomic, copy) void (^completedWithResult)(bool);
|
||||
- (instancetype)initWithAuthKeyInfoSelector:(MTDatacenterAuthInfoSelector)authKeyInfoSelector isCdn:(bool)isCdn completion:(void (^)(MTDatacenterAuthAction *, bool))completion;
|
||||
|
||||
- (instancetype)initWithTempAuth:(bool)tempAuth tempAuthKeyType:(MTDatacenterAuthTempKeyType)tempAuthKeyType bindKey:(MTDatacenterAuthKey *)bindKey;
|
||||
|
||||
- (void)execute:(MTContext *)context datacenterId:(NSInteger)datacenterId isCdn:(bool)isCdn;
|
||||
- (void)execute:(MTContext *)context datacenterId:(NSInteger)datacenterId;
|
||||
- (void)cancel;
|
||||
|
||||
@end
|
||||
|
||||
@ -1,10 +1,5 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
typedef NS_ENUM(NSUInteger, MTDatacenterAuthTempKeyType) {
|
||||
MTDatacenterAuthTempKeyTypeMain,
|
||||
MTDatacenterAuthTempKeyTypeMedia
|
||||
};
|
||||
|
||||
@interface MTDatacenterAuthKey: NSObject <NSCoding>
|
||||
|
||||
@property (nonatomic, strong, readonly) NSData *authKey;
|
||||
@ -15,24 +10,24 @@ typedef NS_ENUM(NSUInteger, MTDatacenterAuthTempKeyType) {
|
||||
|
||||
@end
|
||||
|
||||
typedef NS_ENUM(int64_t, MTDatacenterAuthInfoSelector) {
|
||||
MTDatacenterAuthInfoSelectorPersistent = 0,
|
||||
MTDatacenterAuthInfoSelectorEphemeralMain,
|
||||
MTDatacenterAuthInfoSelectorEphemeralMedia
|
||||
};
|
||||
|
||||
@interface MTDatacenterAuthInfo : NSObject <NSCoding>
|
||||
|
||||
@property (nonatomic, strong, readonly) NSData *authKey;
|
||||
@property (nonatomic, readonly) int64_t authKeyId;
|
||||
@property (nonatomic, strong, readonly) NSArray *saltSet;
|
||||
@property (nonatomic, strong, readonly) NSDictionary *authKeyAttributes;
|
||||
@property (nonatomic, strong, readonly) MTDatacenterAuthKey *mainTempAuthKey;
|
||||
@property (nonatomic, strong, readonly) MTDatacenterAuthKey *mediaTempAuthKey;
|
||||
|
||||
@property (nonatomic, strong, readonly) MTDatacenterAuthKey *persistentAuthKey;
|
||||
|
||||
- (instancetype)initWithAuthKey:(NSData *)authKey authKeyId:(int64_t)authKeyId saltSet:(NSArray *)saltSet authKeyAttributes:(NSDictionary *)authKeyAttributes mainTempAuthKey:(MTDatacenterAuthKey *)mainTempAuthKey mediaTempAuthKey:(MTDatacenterAuthKey *)mediaTempAuthKey;
|
||||
- (instancetype)initWithAuthKey:(NSData *)authKey authKeyId:(int64_t)authKeyId saltSet:(NSArray *)saltSet authKeyAttributes:(NSDictionary *)authKeyAttributes;
|
||||
|
||||
- (int64_t)authSaltForMessageId:(int64_t)messageId;
|
||||
- (MTDatacenterAuthInfo *)mergeSaltSet:(NSArray *)updatedSaltSet forTimestamp:(NSTimeInterval)timestamp;
|
||||
|
||||
- (MTDatacenterAuthInfo *)withUpdatedAuthKeyAttributes:(NSDictionary *)authKeyAttributes;
|
||||
- (MTDatacenterAuthKey *)tempAuthKeyWithType:(MTDatacenterAuthTempKeyType)type;
|
||||
- (MTDatacenterAuthInfo *)withUpdatedTempAuthKeyWithType:(MTDatacenterAuthTempKeyType)type key:(MTDatacenterAuthKey *)key;
|
||||
|
||||
@end
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <MtProtoKit/MTDatacenterAuthInfo.h>
|
||||
|
||||
@class MTProto;
|
||||
@class MTIncomingMessage;
|
||||
@class MTMessageTransaction;
|
||||
@class MTApiEnvironment;
|
||||
@class MTSessionInfo;
|
||||
|
||||
@protocol MTMessageService <NSObject>
|
||||
|
||||
@ -15,10 +17,10 @@
|
||||
- (void)mtProtoDidAddService:(MTProto *)mtProto;
|
||||
- (void)mtProtoDidRemoveService:(MTProto *)mtProto;
|
||||
- (void)mtProtoPublicKeysUpdated:(MTProto *)mtProto datacenterId:(NSInteger)datacenterId publicKeys:(NSArray<NSDictionary *> *)publicKeys;
|
||||
- (MTMessageTransaction *)mtProtoMessageTransaction:(MTProto *)mtProto;
|
||||
- (MTMessageTransaction *)mtProtoMessageTransaction:(MTProto *)mtProto authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector sessionInfo:(MTSessionInfo *)sessionInfo;
|
||||
- (void)mtProtoDidChangeSession:(MTProto *)mtProto;
|
||||
- (void)mtProtoServerDidChangeSession:(MTProto *)mtProto firstValidMessageId:(int64_t)firstValidMessageId otherValidMessageIds:(NSArray *)otherValidMessageIds;
|
||||
- (void)mtProto:(MTProto *)mtProto receivedMessage:(MTIncomingMessage *)message;
|
||||
- (void)mtProto:(MTProto *)mtProto receivedMessage:(MTIncomingMessage *)message authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector;
|
||||
- (void)mtProto:(MTProto *)mtProto receivedQuickAck:(int32_t)quickAckId;
|
||||
- (void)mtProto:(MTProto *)mtProto transactionsMayHaveFailed:(NSArray *)transactionIds;
|
||||
- (void)mtProtoAllTransactionsMayHaveFailed:(MTProto *)mtProto;
|
||||
|
||||
@ -71,4 +71,6 @@
|
||||
|
||||
- (void)_messageResendRequestFailed:(int64_t)messageId;
|
||||
|
||||
+ (NSData *)_manuallyEncryptedMessage:(NSData *)preparedData messageId:(int64_t)messageId authKey:(MTDatacenterAuthKey *)authKey;
|
||||
|
||||
@end
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <MtProtoKit/MTProtoPersistenceInterface.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface MTProtoEngine : NSObject
|
||||
|
||||
- (instancetype)initWithPersistenceInterface:(id<MTProtoPersistenceInterface>)persistenceInterface;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <MtProtoKit/MTProtoEngine.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface MTProtoInstance : NSObject
|
||||
|
||||
- (instancetype)initWithEngine:(MTProtoEngine *)engine;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@ -0,0 +1,14 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class MTSignal;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol MTProtoPersistenceInterface <NSObject>
|
||||
|
||||
- (MTSignal *)get:(NSData *)key;
|
||||
- (void)set:(NSData *)key value:(NSData *)value;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
156
submodules/MtProtoKit/Sources/MTBindKeyMessageService.m
Normal file
156
submodules/MtProtoKit/Sources/MTBindKeyMessageService.m
Normal file
@ -0,0 +1,156 @@
|
||||
#import <MtProtoKit/MTBindKeyMessageService.h>
|
||||
|
||||
#import <MtProtoKit/MTTime.h>
|
||||
#import <MtProtoKit/MTContext.h>
|
||||
#import <MtProtoKit/MTProto.h>
|
||||
#import <MtProtoKit/MTSerialization.h>
|
||||
#import <MtProtoKit/MTOutgoingMessage.h>
|
||||
#import <MtProtoKit/MTIncomingMessage.h>
|
||||
#import <MtProtoKit/MTPreparedMessage.h>
|
||||
#import <MtProtoKit/MTMessageTransaction.h>
|
||||
#import <MtProtoKit/MTDatacenterSaltInfo.h>
|
||||
#import <MtProtoKit/MTSessionInfo.h>
|
||||
#import "MTInternalMessageParser.h"
|
||||
#import "MTRpcResultMessage.h"
|
||||
#import "MTBuffer.h"
|
||||
|
||||
@interface MTBindKeyMessageService () {
|
||||
MTDatacenterAuthKey *_persistentKey;
|
||||
MTDatacenterAuthKey *_ephemeralKey;
|
||||
void (^_completion)(bool);
|
||||
|
||||
int64_t _currentMessageId;
|
||||
id _currentTransactionId;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTBindKeyMessageService
|
||||
|
||||
- (instancetype)initWithPersistentKey:(MTDatacenterAuthKey *)persistentKey ephemeralKey:(MTDatacenterAuthKey *)ephemeralKey completion:(void (^)(bool))completion {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_persistentKey = persistentKey;
|
||||
_ephemeralKey = ephemeralKey;
|
||||
_completion = [completion copy];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)mtProtoDidAddService:(MTProto *)mtProto
|
||||
{
|
||||
[mtProto requestTransportTransaction];
|
||||
}
|
||||
|
||||
- (MTMessageTransaction *)mtProtoMessageTransaction:(MTProto *)mtProto authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector sessionInfo:(MTSessionInfo *)sessionInfo
|
||||
{
|
||||
if (_currentTransactionId != nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
int64_t bindingMessageId = [sessionInfo generateClientMessageId:NULL];
|
||||
int32_t bindingSeqNo = [sessionInfo takeSeqNo:true];
|
||||
|
||||
int32_t expiresAt = (int32_t)([mtProto.context globalTime] + mtProto.context.tempKeyExpiration);
|
||||
|
||||
int64_t randomId = 0;
|
||||
arc4random_buf(&randomId, 8);
|
||||
|
||||
int64_t nonce = 0;
|
||||
arc4random_buf(&nonce, 8);
|
||||
|
||||
MTBuffer *decryptedMessage = [[MTBuffer alloc] init];
|
||||
//bind_auth_key_inner#75a3f765 nonce:long temp_auth_key_id:long perm_auth_key_id:long temp_session_id:long expires_at:int = BindAuthKeyInner;
|
||||
[decryptedMessage appendInt32:(int32_t)0x75a3f765];
|
||||
[decryptedMessage appendInt64:nonce];
|
||||
[decryptedMessage appendInt64:_ephemeralKey.authKeyId];
|
||||
[decryptedMessage appendInt64:_persistentKey.authKeyId];
|
||||
[decryptedMessage appendInt64:sessionInfo.sessionId];
|
||||
[decryptedMessage appendInt32:expiresAt];
|
||||
|
||||
NSData *encryptedMessage = [MTProto _manuallyEncryptedMessage:[decryptedMessage data] messageId:bindingMessageId authKey:_persistentKey];
|
||||
|
||||
MTBuffer *bindRequestData = [[MTBuffer alloc] init];
|
||||
|
||||
//auth.bindTempAuthKey#cdd42a05 perm_auth_key_id:long nonce:long expires_at:int encrypted_message:bytes = Bool;
|
||||
|
||||
[bindRequestData appendInt32:(int32_t)0xcdd42a05];
|
||||
[bindRequestData appendInt64:_persistentKey.authKeyId];
|
||||
[bindRequestData appendInt64:nonce];
|
||||
[bindRequestData appendInt32:expiresAt];
|
||||
[bindRequestData appendTLBytes:encryptedMessage];
|
||||
|
||||
MTOutgoingMessage *outgoingMessage = [[MTOutgoingMessage alloc] initWithData:bindRequestData.data metadata:[NSString stringWithFormat:@"auth.bindTempAuthKey"] additionalDebugDescription:nil shortMetadata:@"auth.bindTempAuthKey" messageId:bindingMessageId messageSeqNo:bindingSeqNo];
|
||||
|
||||
return [[MTMessageTransaction alloc] initWithMessagePayload:@[outgoingMessage] prepared:nil failed:nil completion:^(NSDictionary *messageInternalIdToTransactionId, NSDictionary *messageInternalIdToPreparedMessage, __unused NSDictionary *messageInternalIdToQuickAckId) {
|
||||
MTPreparedMessage *preparedMessage = messageInternalIdToPreparedMessage[outgoingMessage.internalId];
|
||||
if (preparedMessage != nil && messageInternalIdToTransactionId[outgoingMessage.internalId] != nil) {
|
||||
_currentMessageId = preparedMessage.messageId;
|
||||
_currentTransactionId = messageInternalIdToTransactionId[outgoingMessage.internalId];
|
||||
}
|
||||
}];
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)mtProto:(MTProto *)__unused mtProto messageDeliveryFailed:(int64_t)messageId {
|
||||
if (messageId == _currentMessageId) {
|
||||
_currentMessageId = 0;
|
||||
_currentTransactionId = nil;
|
||||
|
||||
[mtProto requestTransportTransaction];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)mtProto:(MTProto *)mtProto transactionsMayHaveFailed:(NSArray *)transactionIds {
|
||||
if (_currentTransactionId != nil && [transactionIds containsObject:_currentTransactionId]) {
|
||||
_currentTransactionId = nil;
|
||||
|
||||
[mtProto requestTransportTransaction];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)mtProtoAllTransactionsMayHaveFailed:(MTProto *)mtProto {
|
||||
if (_currentTransactionId != nil) {
|
||||
_currentTransactionId = nil;
|
||||
|
||||
[mtProto requestTransportTransaction];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)mtProtoDidChangeSession:(MTProto *)mtProto {
|
||||
_currentMessageId = 0;
|
||||
_currentTransactionId = nil;
|
||||
|
||||
[mtProto requestTransportTransaction];
|
||||
}
|
||||
|
||||
- (void)mtProtoServerDidChangeSession:(MTProto *)mtProto firstValidMessageId:(int64_t)firstValidMessageId messageIdsInFirstValidContainer:(NSArray *)messageIdsInFirstValidContainer {
|
||||
if (_currentMessageId != 0 && _currentMessageId < firstValidMessageId && ![messageIdsInFirstValidContainer containsObject:@(_currentMessageId)]) {
|
||||
_currentMessageId = 0;
|
||||
_currentTransactionId = nil;
|
||||
|
||||
[mtProto requestTransportTransaction];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)mtProto:(MTProto *)mtProto receivedMessage:(MTIncomingMessage *)message authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector {
|
||||
if ([message.body isKindOfClass:[MTRpcResultMessage class]]) {
|
||||
MTRpcResultMessage *rpcResultMessage = message.body;
|
||||
if (rpcResultMessage.requestMessageId == _currentMessageId) {
|
||||
bool success = false;
|
||||
if (rpcResultMessage.data.length >= 4) {
|
||||
uint32_t signature = 0;
|
||||
[rpcResultMessage.data getBytes:&signature range:NSMakeRange(0, 4)];
|
||||
|
||||
//boolTrue#997275b5 = Bool;
|
||||
if (signature == 0x997275b5U) {
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
_completion(success);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@ -114,7 +114,34 @@
|
||||
|
||||
@end
|
||||
|
||||
@interface MTContext () <MTDiscoverDatacenterAddressActionDelegate, MTDatacenterAuthActionDelegate, MTDatacenterTransferAuthActionDelegate>
|
||||
typedef int64_t MTDatacenterAuthInfoMapKey;
|
||||
|
||||
typedef struct {
|
||||
int32_t datacenterId;
|
||||
MTDatacenterAuthInfoSelector selector;
|
||||
} MTDatacenterAuthInfoMapKeyStruct;
|
||||
|
||||
static MTDatacenterAuthInfoMapKey authInfoMapKey(int32_t datacenterId, MTDatacenterAuthInfoSelector selector) {
|
||||
int64_t result = (((int64_t)(selector)) << 32) | ((int64_t)(datacenterId));
|
||||
return result;
|
||||
}
|
||||
|
||||
static NSNumber *authInfoMapIntegerKey(int32_t datacenterId, MTDatacenterAuthInfoSelector selector) {
|
||||
return [NSNumber numberWithLongLong:authInfoMapKey(datacenterId, selector)];
|
||||
}
|
||||
|
||||
static MTDatacenterAuthInfoMapKeyStruct parseAuthInfoMapKey(int64_t key) {
|
||||
MTDatacenterAuthInfoMapKeyStruct result;
|
||||
result.datacenterId = (int32_t)(key & 0x7fffffff);
|
||||
result.selector = (int32_t)(((key >> 32) & 0x7fffffff));
|
||||
return result;
|
||||
}
|
||||
|
||||
static MTDatacenterAuthInfoMapKeyStruct parseAuthInfoMapKeyInteger(NSNumber *key) {
|
||||
return parseAuthInfoMapKey([key longLongValue]);
|
||||
}
|
||||
|
||||
@interface MTContext () <MTDiscoverDatacenterAddressActionDelegate, MTDatacenterTransferAuthActionDelegate>
|
||||
{
|
||||
int64_t _uniqueId;
|
||||
|
||||
@ -127,7 +154,7 @@
|
||||
NSMutableDictionary<NSNumber *, NSMutableDictionary<MTDatacenterAddress *, MTTransportSchemeStats *> *> *_transportSchemeStats;
|
||||
MTTimer *_schemeStatsSyncTimer;
|
||||
|
||||
NSMutableDictionary *_datacenterAuthInfoById;
|
||||
NSMutableDictionary<NSNumber *, MTDatacenterAuthInfo *> *_datacenterAuthInfoById;
|
||||
|
||||
NSMutableDictionary *_datacenterPublicKeysById;
|
||||
|
||||
@ -138,8 +165,7 @@
|
||||
MTSignal *_discoverBackupAddressListSignal;
|
||||
|
||||
NSMutableDictionary *_discoverDatacenterAddressActions;
|
||||
NSMutableDictionary *_datacenterAuthActions;
|
||||
NSMutableDictionary *_datacenterTempAuthActions;
|
||||
NSMutableDictionary<NSNumber *, MTDatacenterAuthAction *> *_datacenterAuthActions;
|
||||
NSMutableDictionary *_datacenterTransferAuthActions;
|
||||
|
||||
NSMutableDictionary<NSNumber *, NSNumber *> *_datacenterCheckKeyRemovedActionTimestamps;
|
||||
@ -201,6 +227,11 @@ static int32_t fixedTimeDifferenceValue = 0;
|
||||
_apiEnvironment = apiEnvironment;
|
||||
_isTestingEnvironment = isTestingEnvironment;
|
||||
_useTempAuthKeys = useTempAuthKeys;
|
||||
#if DEBUG
|
||||
_tempKeyExpiration = 1 * 60 * 60;
|
||||
#else
|
||||
_tempKeyExpiration = 1 * 60 * 60;
|
||||
#endif
|
||||
|
||||
_datacenterSeedAddressSetById = [[NSMutableDictionary alloc] init];
|
||||
|
||||
@ -218,7 +249,6 @@ static int32_t fixedTimeDifferenceValue = 0;
|
||||
|
||||
_discoverDatacenterAddressActions = [[NSMutableDictionary alloc] init];
|
||||
_datacenterAuthActions = [[NSMutableDictionary alloc] init];
|
||||
_datacenterTempAuthActions = [[NSMutableDictionary alloc] init];
|
||||
_datacenterTransferAuthActions = [[NSMutableDictionary alloc] init];
|
||||
_datacenterCheckKeyRemovedActionTimestamps = [[NSMutableDictionary alloc] init];
|
||||
_datacenterCheckKeyRemovedActions = [[NSMutableDictionary alloc] init];
|
||||
@ -258,9 +288,6 @@ static int32_t fixedTimeDifferenceValue = 0;
|
||||
NSDictionary *datacenterAuthActions = _datacenterAuthActions;
|
||||
_datacenterAuthActions = nil;
|
||||
|
||||
NSDictionary *datacenterTempAuthActions = _datacenterTempAuthActions;
|
||||
_datacenterTempAuthActions = nil;
|
||||
|
||||
NSDictionary *discoverDatacenterAddressActions = _discoverDatacenterAddressActions;
|
||||
_discoverDatacenterAddressActions = nil;
|
||||
|
||||
@ -284,17 +311,9 @@ static int32_t fixedTimeDifferenceValue = 0;
|
||||
[action cancel];
|
||||
}
|
||||
|
||||
for (NSNumber *nDatacenterId in datacenterAuthActions)
|
||||
for (NSNumber *key in datacenterAuthActions)
|
||||
{
|
||||
MTDatacenterAuthAction *action = datacenterAuthActions[nDatacenterId];
|
||||
action.delegate = nil;
|
||||
[action cancel];
|
||||
}
|
||||
|
||||
for (NSNumber *nDatacenterId in datacenterTempAuthActions)
|
||||
{
|
||||
MTDatacenterAuthAction *action = datacenterTempAuthActions[nDatacenterId];
|
||||
action.delegate = nil;
|
||||
MTDatacenterAuthAction *action = datacenterAuthActions[key];
|
||||
[action cancel];
|
||||
}
|
||||
|
||||
@ -360,6 +379,14 @@ static int32_t fixedTimeDifferenceValue = 0;
|
||||
NSDictionary *datacenterAuthInfoById = [keychain objectForKey:@"datacenterAuthInfoById" group:@"persistent"];
|
||||
if (datacenterAuthInfoById != nil) {
|
||||
_datacenterAuthInfoById = [[NSMutableDictionary alloc] initWithDictionary:datacenterAuthInfoById];
|
||||
#if DEBUG
|
||||
/*NSArray<NSNumber *> *keys = [_datacenterAuthInfoById allKeys];
|
||||
for (NSNumber *key in keys) {
|
||||
if (parseAuthInfoMapKeyInteger(key).selector != MTDatacenterAuthInfoSelectorPersistent) {
|
||||
[_datacenterAuthInfoById removeObjectForKey:key];
|
||||
}
|
||||
}*/
|
||||
#endif
|
||||
}
|
||||
|
||||
NSDictionary *datacenterPublicKeysById = [keychain objectForKey:@"datacenterPublicKeysById" group:@"ephemeral"];
|
||||
@ -582,29 +609,46 @@ static int32_t fixedTimeDifferenceValue = 0;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)updateAuthInfoForDatacenterWithId:(NSInteger)datacenterId authInfo:(MTDatacenterAuthInfo *)authInfo
|
||||
- (void)updateAuthInfoForDatacenterWithId:(NSInteger)datacenterId authInfo:(MTDatacenterAuthInfo *)authInfo selector:(MTDatacenterAuthInfoSelector)selector
|
||||
{
|
||||
[[MTContext contextQueue] dispatchOnQueue:^
|
||||
{
|
||||
if (datacenterId != 0)
|
||||
{
|
||||
if (MTLogEnabled()) {
|
||||
MTLog(@"[MTContext#%x: auth info updated for %d to %@]", (int)self, datacenterId, authInfo);
|
||||
}
|
||||
NSNumber *infoKey = authInfoMapIntegerKey((int32_t)datacenterId, selector);
|
||||
|
||||
bool wasNil = _datacenterAuthInfoById[infoKey] == nil;
|
||||
|
||||
if (authInfo != nil) {
|
||||
_datacenterAuthInfoById[@(datacenterId)] = authInfo;
|
||||
_datacenterAuthInfoById[infoKey] = authInfo;
|
||||
} else {
|
||||
[_datacenterAuthInfoById removeObjectForKey:@(datacenterId)];
|
||||
if (_datacenterAuthInfoById[infoKey] == nil) {
|
||||
return;
|
||||
}
|
||||
[_datacenterAuthInfoById removeObjectForKey:infoKey];
|
||||
}
|
||||
|
||||
if (MTLogEnabled()) {
|
||||
MTLog(@"[MTContext#%x: auth info updated for %d selector %d to %@]", (int)self, datacenterId, selector, authInfo);
|
||||
}
|
||||
|
||||
[_keychain setObject:_datacenterAuthInfoById forKey:@"datacenterAuthInfoById" group:@"persistent"];
|
||||
|
||||
NSArray *currentListeners = [[NSArray alloc] initWithArray:_changeListeners];
|
||||
|
||||
for (id<MTContextChangeListener> listener in currentListeners)
|
||||
{
|
||||
if ([listener respondsToSelector:@selector(contextDatacenterAuthInfoUpdated:datacenterId:authInfo:)])
|
||||
[listener contextDatacenterAuthInfoUpdated:self datacenterId:datacenterId authInfo:authInfo];
|
||||
if ([listener respondsToSelector:@selector(contextDatacenterAuthInfoUpdated:datacenterId:authInfo:selector:)])
|
||||
[listener contextDatacenterAuthInfoUpdated:self datacenterId:datacenterId authInfo:authInfo selector:selector];
|
||||
}
|
||||
|
||||
if (wasNil && authInfo != nil && selector == MTDatacenterAuthInfoSelectorPersistent) {
|
||||
for (NSNumber *key in _datacenterAuthActions) {
|
||||
MTDatacenterAuthInfoMapKeyStruct parsedKey = parseAuthInfoMapKeyInteger(key);
|
||||
if (parsedKey.datacenterId == datacenterId && parsedKey.selector != MTDatacenterAuthInfoSelectorPersistent) {
|
||||
[_datacenterAuthActions[key] execute:self datacenterId:datacenterId];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}];
|
||||
@ -864,10 +908,11 @@ static int32_t fixedTimeDifferenceValue = 0;
|
||||
return results;
|
||||
}
|
||||
|
||||
- (MTDatacenterAuthInfo *)authInfoForDatacenterWithId:(NSInteger)datacenterId {
|
||||
- (MTDatacenterAuthInfo *)authInfoForDatacenterWithId:(NSInteger)datacenterId selector:(MTDatacenterAuthInfoSelector)selector {
|
||||
__block MTDatacenterAuthInfo *result = nil;
|
||||
[[MTContext contextQueue] dispatchOnQueue:^{
|
||||
result = _datacenterAuthInfoById[@(datacenterId)];
|
||||
NSNumber *infoKey = authInfoMapIntegerKey((int32_t)datacenterId, selector);
|
||||
result = _datacenterAuthInfoById[infoKey];
|
||||
} synchronous:true];
|
||||
|
||||
return result;
|
||||
@ -1251,48 +1296,44 @@ static int32_t fixedTimeDifferenceValue = 0;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)authInfoForDatacenterWithIdRequired:(NSInteger)datacenterId isCdn:(bool)isCdn
|
||||
- (void)authInfoForDatacenterWithIdRequired:(NSInteger)datacenterId isCdn:(bool)isCdn selector:(MTDatacenterAuthInfoSelector)selector
|
||||
{
|
||||
[[MTContext contextQueue] dispatchOnQueue:^
|
||||
{
|
||||
if (_datacenterAuthActions[@(datacenterId)] == nil)
|
||||
NSNumber *infoKey = authInfoMapIntegerKey((int32_t)datacenterId, selector);
|
||||
|
||||
if (_datacenterAuthActions[infoKey] == nil)
|
||||
{
|
||||
MTDatacenterAuthAction *authAction = [[MTDatacenterAuthAction alloc] initWithTempAuth:false tempAuthKeyType:MTDatacenterAuthTempKeyTypeMain bindKey:nil];
|
||||
authAction.delegate = self;
|
||||
_datacenterAuthActions[@(datacenterId)] = authAction;
|
||||
[authAction execute:self datacenterId:datacenterId isCdn:isCdn];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)tempAuthKeyForDatacenterWithIdRequired:(NSInteger)datacenterId keyType:(MTDatacenterAuthTempKeyType)keyType {
|
||||
[[MTContext contextQueue] dispatchOnQueue:^{
|
||||
if (_datacenterTempAuthActions[@(datacenterId)] == nil) {
|
||||
MTDatacenterAuthAction *authAction = [[MTDatacenterAuthAction alloc] initWithTempAuth:true tempAuthKeyType:keyType bindKey:nil];
|
||||
authAction.delegate = self;
|
||||
_datacenterTempAuthActions[@(datacenterId)] = authAction;
|
||||
[authAction execute:self datacenterId:datacenterId isCdn:false];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)datacenterAuthActionCompleted:(MTDatacenterAuthAction *)action
|
||||
{
|
||||
[[MTContext contextQueue] dispatchOnQueue:^
|
||||
{
|
||||
if (action.tempAuth) {
|
||||
for (NSNumber *nDatacenterId in _datacenterTempAuthActions) {
|
||||
if (_datacenterTempAuthActions[nDatacenterId] == action) {
|
||||
[_datacenterTempAuthActions removeObjectForKey:nDatacenterId];
|
||||
__weak MTContext *weakSelf = self;
|
||||
MTDatacenterAuthAction *authAction = [[MTDatacenterAuthAction alloc] initWithAuthKeyInfoSelector:selector isCdn:isCdn completion:^(MTDatacenterAuthAction *action, __unused bool success) {
|
||||
[[MTContext contextQueue] dispatchOnQueue:^{
|
||||
__strong MTContext *strongSelf = weakSelf;
|
||||
if (strongSelf == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (NSNumber *key in _datacenterAuthActions) {
|
||||
if (_datacenterAuthActions[key] == action) {
|
||||
[_datacenterAuthActions removeObjectForKey:key];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}];
|
||||
}];
|
||||
_datacenterAuthActions[infoKey] = authAction;
|
||||
|
||||
switch (selector) {
|
||||
case MTDatacenterAuthInfoSelectorEphemeralMain:
|
||||
case MTDatacenterAuthInfoSelectorEphemeralMedia: {
|
||||
if ([self authInfoForDatacenterWithId:datacenterId selector:MTDatacenterAuthInfoSelectorPersistent] == nil) {
|
||||
[self authInfoForDatacenterWithIdRequired:datacenterId isCdn:false selector:MTDatacenterAuthInfoSelectorPersistent];
|
||||
} else {
|
||||
[authAction execute:self datacenterId:datacenterId];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (NSNumber *nDatacenterId in _datacenterAuthActions) {
|
||||
if (_datacenterAuthActions[nDatacenterId] == action) {
|
||||
[_datacenterAuthActions removeObjectForKey:nDatacenterId];
|
||||
|
||||
default: {
|
||||
[authAction execute:self datacenterId:datacenterId];
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -1357,21 +1398,11 @@ static int32_t fixedTimeDifferenceValue = 0;
|
||||
|
||||
- (void)updatePeriodicTasks
|
||||
{
|
||||
[[MTContext contextQueue] dispatchOnQueue:^
|
||||
{
|
||||
int64_t saltsRequiredAtLeastUntilMessageId = (int64_t)(([self globalTime] + 24 * 60.0 * 60.0) * 4294967296);
|
||||
|
||||
[_datacenterAuthInfoById enumerateKeysAndObjectsUsingBlock:^(NSNumber *nDatacenterId, MTDatacenterAuthInfo *authInfo, __unused BOOL *stop)
|
||||
{
|
||||
if ([authInfo authSaltForMessageId:saltsRequiredAtLeastUntilMessageId == 0]) {
|
||||
}
|
||||
}];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)checkIfLoggedOut:(NSInteger)datacenterId {
|
||||
[[MTContext contextQueue] dispatchOnQueue:^{
|
||||
MTDatacenterAuthInfo *authInfo = [self authInfoForDatacenterWithId:datacenterId];
|
||||
MTDatacenterAuthInfo *authInfo = [self authInfoForDatacenterWithId:datacenterId selector:MTDatacenterAuthInfoSelectorPersistent];
|
||||
if (authInfo == nil || authInfo.authKey == nil) {
|
||||
return;
|
||||
}
|
||||
@ -1382,7 +1413,7 @@ static int32_t fixedTimeDifferenceValue = 0;
|
||||
_datacenterCheckKeyRemovedActionTimestamps[@(datacenterId)] = currentTimestamp;
|
||||
[_datacenterCheckKeyRemovedActions[@(datacenterId)] dispose];
|
||||
__weak MTContext *weakSelf = self;
|
||||
_datacenterCheckKeyRemovedActions[@(datacenterId)] = [[MTDiscoverConnectionSignals checkIfAuthKeyRemovedWithContext:self datacenterId:datacenterId authKey:authInfo.authKey] startWithNext:^(NSNumber *isRemoved) {
|
||||
_datacenterCheckKeyRemovedActions[@(datacenterId)] = [[MTDiscoverConnectionSignals checkIfAuthKeyRemovedWithContext:self datacenterId:datacenterId authKey:[[MTDatacenterAuthKey alloc] initWithAuthKey:authInfo.authKey authKeyId:authInfo.authKeyId notBound:false]] startWithNext:^(NSNumber* isRemoved) {
|
||||
[[MTContext contextQueue] dispatchOnQueue:^{
|
||||
__strong MTContext *strongSelf = weakSelf;
|
||||
if (strongSelf == nil) {
|
||||
|
||||
@ -12,13 +12,15 @@
|
||||
#import <MtProtoKit/MTSignal.h>
|
||||
#import <MtProtoKit/MTDatacenterAuthMessageService.h>
|
||||
#import <MtProtoKit/MTRequestMessageService.h>
|
||||
#import <MtProtoKit/MTBindKeyMessageService.h>
|
||||
#import "MTBuffer.h"
|
||||
|
||||
@interface MTDatacenterAuthAction () <MTDatacenterAuthMessageServiceDelegate>
|
||||
{
|
||||
void (^_completion)(MTDatacenterAuthAction *, bool);
|
||||
|
||||
bool _isCdn;
|
||||
MTDatacenterAuthTempKeyType _tempAuthKeyType;
|
||||
MTDatacenterAuthKey *_bindKey;
|
||||
MTDatacenterAuthInfoSelector _authKeyInfoSelector;
|
||||
|
||||
NSInteger _datacenterId;
|
||||
__weak MTContext *_context;
|
||||
@ -26,71 +28,61 @@
|
||||
bool _awaitingAddresSetUpdate;
|
||||
MTProto *_authMtProto;
|
||||
MTProto *_bindMtProto;
|
||||
|
||||
MTMetaDisposable *_verifyDisposable;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTDatacenterAuthAction
|
||||
|
||||
- (instancetype)initWithTempAuth:(bool)tempAuth tempAuthKeyType:(MTDatacenterAuthTempKeyType)tempAuthKeyType bindKey:(MTDatacenterAuthKey *)bindKey {
|
||||
- (instancetype)initWithAuthKeyInfoSelector:(MTDatacenterAuthInfoSelector)authKeyInfoSelector isCdn:(bool)isCdn completion:(void (^)(MTDatacenterAuthAction *, bool))completion {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_tempAuth = tempAuth;
|
||||
_tempAuthKeyType = tempAuthKeyType;
|
||||
_bindKey = bindKey;
|
||||
_verifyDisposable = [[MTMetaDisposable alloc] init];
|
||||
_authKeyInfoSelector = authKeyInfoSelector;
|
||||
_isCdn = isCdn;
|
||||
_completion = [completion copy];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
- (void)dealloc {
|
||||
[self cleanup];
|
||||
}
|
||||
|
||||
- (void)execute:(MTContext *)context datacenterId:(NSInteger)datacenterId isCdn:(bool)isCdn
|
||||
{
|
||||
- (void)execute:(MTContext *)context datacenterId:(NSInteger)datacenterId {
|
||||
_datacenterId = datacenterId;
|
||||
_context = context;
|
||||
_isCdn = isCdn;
|
||||
|
||||
if (_datacenterId != 0 && context != nil)
|
||||
{
|
||||
bool alreadyCompleted = false;
|
||||
MTDatacenterAuthInfo *currentAuthInfo = [context authInfoForDatacenterWithId:_datacenterId];
|
||||
if (currentAuthInfo != nil && _bindKey == nil) {
|
||||
if (_tempAuth) {
|
||||
if ([currentAuthInfo tempAuthKeyWithType:_tempAuthKeyType] != nil) {
|
||||
alreadyCompleted = true;
|
||||
}
|
||||
} else {
|
||||
alreadyCompleted = true;
|
||||
}
|
||||
|
||||
MTDatacenterAuthInfo *currentAuthInfo = [context authInfoForDatacenterWithId:_datacenterId selector:_authKeyInfoSelector];
|
||||
if (currentAuthInfo != nil) {
|
||||
alreadyCompleted = true;
|
||||
}
|
||||
|
||||
if (alreadyCompleted) {
|
||||
[self complete];
|
||||
} else {
|
||||
_authMtProto = [[MTProto alloc] initWithContext:context datacenterId:_datacenterId usageCalculationInfo:nil requiredAuthToken:nil authTokenMasterDatacenterId:0];
|
||||
_authMtProto.cdn = isCdn;
|
||||
_authMtProto.cdn = _isCdn;
|
||||
_authMtProto.useUnauthorizedMode = true;
|
||||
if (_tempAuth) {
|
||||
switch (_tempAuthKeyType) {
|
||||
case MTDatacenterAuthTempKeyTypeMain:
|
||||
_authMtProto.media = false;
|
||||
break;
|
||||
case MTDatacenterAuthTempKeyTypeMedia:
|
||||
_authMtProto.media = true;
|
||||
_authMtProto.enforceMedia = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
bool tempAuth = false;
|
||||
switch (_authKeyInfoSelector) {
|
||||
case MTDatacenterAuthInfoSelectorEphemeralMain:
|
||||
tempAuth = true;
|
||||
_authMtProto.media = false;
|
||||
break;
|
||||
case MTDatacenterAuthInfoSelectorEphemeralMedia:
|
||||
tempAuth = true;
|
||||
_authMtProto.media = true;
|
||||
_authMtProto.enforceMedia = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
MTDatacenterAuthMessageService *authService = [[MTDatacenterAuthMessageService alloc] initWithContext:context tempAuth:_tempAuth];
|
||||
MTDatacenterAuthMessageService *authService = [[MTDatacenterAuthMessageService alloc] initWithContext:context tempAuth:tempAuth];
|
||||
authService.delegate = self;
|
||||
[_authMtProto addMessageService:authService];
|
||||
}
|
||||
@ -104,45 +96,71 @@
|
||||
}
|
||||
|
||||
- (void)completeWithAuthKey:(MTDatacenterAuthKey *)authKey timestamp:(int64_t)timestamp {
|
||||
if (_tempAuth) {
|
||||
MTContext *mainContext = _context;
|
||||
if (mainContext != nil) {
|
||||
if (_bindKey != nil) {
|
||||
_bindMtProto = [[MTProto alloc] initWithContext:mainContext datacenterId:_datacenterId usageCalculationInfo:nil requiredAuthToken:nil authTokenMasterDatacenterId:0];
|
||||
_bindMtProto.cdn = false;
|
||||
_bindMtProto.useUnauthorizedMode = false;
|
||||
_bindMtProto.useTempAuthKeys = true;
|
||||
__weak MTDatacenterAuthAction *weakSelf = self;
|
||||
_bindMtProto.tempAuthKeyBindingResultUpdated = ^(bool success) {
|
||||
__strong MTDatacenterAuthAction *strongSelf = weakSelf;
|
||||
if (strongSelf == nil) {
|
||||
return;
|
||||
if (MTLogEnabled()) {
|
||||
MTLog(@"[MTDatacenterAuthAction#%p@%p: completeWithAuthKey %lld selector %d]", self, _context, authKey.authKeyId, _authKeyInfoSelector);
|
||||
}
|
||||
|
||||
switch (_authKeyInfoSelector) {
|
||||
case MTDatacenterAuthInfoSelectorPersistent: {
|
||||
MTDatacenterAuthInfo *authInfo = [[MTDatacenterAuthInfo alloc] initWithAuthKey:authKey.authKey authKeyId:authKey.authKeyId saltSet:@[[[MTDatacenterSaltInfo alloc] initWithSalt:0 firstValidMessageId:timestamp lastValidMessageId:timestamp + (29.0 * 60.0) * 4294967296]] authKeyAttributes:nil];
|
||||
|
||||
MTContext *context = _context;
|
||||
[context updateAuthInfoForDatacenterWithId:_datacenterId authInfo:authInfo selector:_authKeyInfoSelector];
|
||||
[self complete];
|
||||
}
|
||||
break;
|
||||
|
||||
case MTDatacenterAuthInfoSelectorEphemeralMain:
|
||||
case MTDatacenterAuthInfoSelectorEphemeralMedia: {
|
||||
MTContext *mainContext = _context;
|
||||
if (mainContext != nil) {
|
||||
MTDatacenterAuthInfo *persistentAuthInfo = [mainContext authInfoForDatacenterWithId:_datacenterId selector:MTDatacenterAuthInfoSelectorPersistent];
|
||||
if (persistentAuthInfo != nil) {
|
||||
_bindMtProto = [[MTProto alloc] initWithContext:mainContext datacenterId:_datacenterId usageCalculationInfo:nil requiredAuthToken:nil authTokenMasterDatacenterId:0];
|
||||
_bindMtProto.cdn = false;
|
||||
_bindMtProto.useUnauthorizedMode = false;
|
||||
_bindMtProto.useTempAuthKeys = true;
|
||||
_bindMtProto.useExplicitAuthKey = authKey;
|
||||
|
||||
switch (_authKeyInfoSelector) {
|
||||
case MTDatacenterAuthInfoSelectorEphemeralMain:
|
||||
_bindMtProto.media = false;
|
||||
break;
|
||||
case MTDatacenterAuthInfoSelectorEphemeralMedia:
|
||||
_bindMtProto.media = true;
|
||||
_bindMtProto.enforceMedia = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
[strongSelf->_bindMtProto stop];
|
||||
if (strongSelf->_completedWithResult) {
|
||||
strongSelf->_completedWithResult(success);
|
||||
}
|
||||
};
|
||||
_bindMtProto.useExplicitAuthKey = authKey;
|
||||
[_bindMtProto resume];
|
||||
} else {
|
||||
MTContext *context = _context;
|
||||
[context performBatchUpdates:^{
|
||||
MTDatacenterAuthInfo *authInfo = [context authInfoForDatacenterWithId:_datacenterId];
|
||||
if (authInfo != nil) {
|
||||
authInfo = [authInfo withUpdatedTempAuthKeyWithType:_tempAuthKeyType key:authKey];
|
||||
[context updateAuthInfoForDatacenterWithId:_datacenterId authInfo:authInfo];
|
||||
}
|
||||
}];
|
||||
[self complete];
|
||||
|
||||
__weak MTDatacenterAuthAction *weakSelf = self;
|
||||
[_bindMtProto addMessageService:[[MTBindKeyMessageService alloc] initWithPersistentKey:[[MTDatacenterAuthKey alloc] initWithAuthKey:persistentAuthInfo.authKey authKeyId:persistentAuthInfo.authKeyId notBound:false] ephemeralKey:authKey completion:^(bool success) {
|
||||
__strong MTDatacenterAuthAction *strongSelf = weakSelf;
|
||||
if (strongSelf == nil) {
|
||||
return;
|
||||
}
|
||||
[strongSelf->_bindMtProto stop];
|
||||
|
||||
if (success) {
|
||||
MTDatacenterAuthInfo *authInfo = [[MTDatacenterAuthInfo alloc] initWithAuthKey:authKey.authKey authKeyId:authKey.authKeyId saltSet:@[[[MTDatacenterSaltInfo alloc] initWithSalt:0 firstValidMessageId:timestamp lastValidMessageId:timestamp + (29.0 * 60.0) * 4294967296]] authKeyAttributes:nil];
|
||||
|
||||
[strongSelf->_context updateAuthInfoForDatacenterWithId:strongSelf->_datacenterId authInfo:authInfo selector:strongSelf->_authKeyInfoSelector];
|
||||
|
||||
[strongSelf complete];
|
||||
} else {
|
||||
[strongSelf fail];
|
||||
}
|
||||
}]];
|
||||
[_bindMtProto resume];
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
MTDatacenterAuthInfo *authInfo = [[MTDatacenterAuthInfo alloc] initWithAuthKey:authKey.authKey authKeyId:authKey.authKeyId saltSet:@[[[MTDatacenterSaltInfo alloc] initWithSalt:0 firstValidMessageId:timestamp lastValidMessageId:timestamp + (29.0 * 60.0) * 4294967296]] authKeyAttributes:nil mainTempAuthKey:nil mediaTempAuthKey:nil];
|
||||
|
||||
MTContext *context = _context;
|
||||
[context updateAuthInfoForDatacenterWithId:_datacenterId authInfo:authInfo];
|
||||
[self complete];
|
||||
break;
|
||||
|
||||
default:
|
||||
assert(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -153,7 +171,10 @@
|
||||
|
||||
[authMtProto stop];
|
||||
|
||||
[_verifyDisposable dispose];
|
||||
MTProto *bindMtProto = _bindMtProto;
|
||||
_bindMtProto = nil;
|
||||
|
||||
[bindMtProto stop];
|
||||
}
|
||||
|
||||
- (void)cancel
|
||||
@ -162,18 +183,17 @@
|
||||
[self fail];
|
||||
}
|
||||
|
||||
- (void)complete
|
||||
{
|
||||
id<MTDatacenterAuthActionDelegate> delegate = _delegate;
|
||||
if ([delegate respondsToSelector:@selector(datacenterAuthActionCompleted:)])
|
||||
[delegate datacenterAuthActionCompleted:self];
|
||||
- (void)complete {
|
||||
if (_completion) {
|
||||
_completion(self, true);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)fail
|
||||
{
|
||||
id<MTDatacenterAuthActionDelegate> delegate = _delegate;
|
||||
if ([delegate respondsToSelector:@selector(datacenterAuthActionCompleted:)])
|
||||
[delegate datacenterAuthActionCompleted:self];
|
||||
if (_completion) {
|
||||
_completion(self, false);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
|
||||
@implementation MTDatacenterAuthInfo
|
||||
|
||||
- (instancetype)initWithAuthKey:(NSData *)authKey authKeyId:(int64_t)authKeyId saltSet:(NSArray *)saltSet authKeyAttributes:(NSDictionary *)authKeyAttributes mainTempAuthKey:(MTDatacenterAuthKey *)mainTempAuthKey mediaTempAuthKey:(MTDatacenterAuthKey *)mediaTempAuthKey
|
||||
- (instancetype)initWithAuthKey:(NSData *)authKey authKeyId:(int64_t)authKeyId saltSet:(NSArray *)saltSet authKeyAttributes:(NSDictionary *)authKeyAttributes
|
||||
{
|
||||
self = [super init];
|
||||
if (self != nil)
|
||||
@ -36,8 +36,6 @@
|
||||
_authKeyId = authKeyId;
|
||||
_saltSet = saltSet;
|
||||
_authKeyAttributes = authKeyAttributes;
|
||||
_mainTempAuthKey = mainTempAuthKey;
|
||||
_mediaTempAuthKey = mediaTempAuthKey;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@ -51,8 +49,6 @@
|
||||
_authKeyId = [aDecoder decodeInt64ForKey:@"authKeyId"];
|
||||
_saltSet = [aDecoder decodeObjectForKey:@"saltSet"];
|
||||
_authKeyAttributes = [aDecoder decodeObjectForKey:@"authKeyAttributes"];
|
||||
_mainTempAuthKey = [aDecoder decodeObjectForKey:@"tempAuthKey"];
|
||||
_mediaTempAuthKey = [aDecoder decodeObjectForKey:@"mediaTempAuthKey"];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@ -63,8 +59,6 @@
|
||||
[aCoder encodeInt64:_authKeyId forKey:@"authKeyId"];
|
||||
[aCoder encodeObject:_saltSet forKey:@"saltSet"];
|
||||
[aCoder encodeObject:_authKeyAttributes forKey:@"authKeyAttributes"];
|
||||
[aCoder encodeObject:_mainTempAuthKey forKey:@"tempAuthKey"];
|
||||
[aCoder encodeObject:_mediaTempAuthKey forKey:@"mediaTempAuthKey"];
|
||||
}
|
||||
|
||||
- (int64_t)authSaltForMessageId:(int64_t)messageId
|
||||
@ -113,35 +107,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
return [[MTDatacenterAuthInfo alloc] initWithAuthKey:_authKey authKeyId:_authKeyId saltSet:mergedSaltSet authKeyAttributes:_authKeyAttributes mainTempAuthKey:_mainTempAuthKey mediaTempAuthKey:_mediaTempAuthKey];
|
||||
return [[MTDatacenterAuthInfo alloc] initWithAuthKey:_authKey authKeyId:_authKeyId saltSet:mergedSaltSet authKeyAttributes:_authKeyAttributes];
|
||||
}
|
||||
|
||||
- (MTDatacenterAuthInfo *)withUpdatedAuthKeyAttributes:(NSDictionary *)authKeyAttributes {
|
||||
return [[MTDatacenterAuthInfo alloc] initWithAuthKey:_authKey authKeyId:_authKeyId saltSet:_saltSet authKeyAttributes:authKeyAttributes mainTempAuthKey:_mainTempAuthKey mediaTempAuthKey:_mediaTempAuthKey];
|
||||
}
|
||||
|
||||
- (MTDatacenterAuthKey *)tempAuthKeyWithType:(MTDatacenterAuthTempKeyType)type {
|
||||
switch (type) {
|
||||
case MTDatacenterAuthTempKeyTypeMain:
|
||||
return _mainTempAuthKey;
|
||||
case MTDatacenterAuthTempKeyTypeMedia:
|
||||
return _mediaTempAuthKey;
|
||||
default:
|
||||
NSAssert(false, @"unknown MTDatacenterAuthTempKeyType");
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (MTDatacenterAuthInfo *)withUpdatedTempAuthKeyWithType:(MTDatacenterAuthTempKeyType)type key:(MTDatacenterAuthKey *)key {
|
||||
switch (type) {
|
||||
case MTDatacenterAuthTempKeyTypeMain:
|
||||
return [[MTDatacenterAuthInfo alloc] initWithAuthKey:_authKey authKeyId:_authKeyId saltSet:_saltSet authKeyAttributes:_authKeyAttributes mainTempAuthKey:key mediaTempAuthKey:_mediaTempAuthKey];
|
||||
case MTDatacenterAuthTempKeyTypeMedia:
|
||||
return [[MTDatacenterAuthInfo alloc] initWithAuthKey:_authKey authKeyId:_authKeyId saltSet:_saltSet authKeyAttributes:_authKeyAttributes mainTempAuthKey:_mainTempAuthKey mediaTempAuthKey:key];
|
||||
default:
|
||||
NSAssert(false, @"unknown MTDatacenterAuthTempKeyType");
|
||||
return self;
|
||||
}
|
||||
return [[MTDatacenterAuthInfo alloc] initWithAuthKey:_authKey authKeyId:_authKeyId saltSet:_saltSet authKeyAttributes:authKeyAttributes];
|
||||
}
|
||||
|
||||
- (MTDatacenterAuthKey *)persistentAuthKey {
|
||||
|
||||
@ -222,7 +222,7 @@ typedef enum {
|
||||
}
|
||||
}
|
||||
|
||||
- (MTMessageTransaction *)mtProtoMessageTransaction:(MTProto *)mtProto
|
||||
- (MTMessageTransaction *)mtProtoMessageTransaction:(MTProto *)mtProto authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector sessionInfo:(MTSessionInfo *)sessionInfo
|
||||
{
|
||||
if (_currentStageTransactionId == nil)
|
||||
{
|
||||
@ -308,7 +308,7 @@ typedef enum {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)mtProto:(MTProto *)mtProto receivedMessage:(MTIncomingMessage *)message
|
||||
- (void)mtProto:(MTProto *)mtProto receivedMessage:(MTIncomingMessage *)message authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector
|
||||
{
|
||||
if (_stage == MTDatacenterAuthStagePQ && [message.body isKindOfClass:[MTResPqMessage class]])
|
||||
{
|
||||
@ -389,7 +389,7 @@ typedef enum {
|
||||
[innerDataBuffer appendBytes:_nonce.bytes length:_nonce.length];
|
||||
[innerDataBuffer appendBytes:_serverNonce.bytes length:_serverNonce.length];
|
||||
[innerDataBuffer appendBytes:_newNonce.bytes length:_newNonce.length];
|
||||
[innerDataBuffer appendInt32:60 * 60 * 32];
|
||||
[innerDataBuffer appendInt32:mtProto.context.tempKeyExpiration];
|
||||
|
||||
NSData *innerDataBytes = innerDataBuffer.data;
|
||||
|
||||
|
||||
@ -249,12 +249,11 @@
|
||||
MTMetaDisposable *disposable = [[MTMetaDisposable alloc] init];
|
||||
|
||||
[[MTContext contextQueue] dispatchOnQueue:^{
|
||||
MTDatacenterAuthAction *action = [[MTDatacenterAuthAction alloc] initWithTempAuth:true tempAuthKeyType:MTDatacenterAuthTempKeyTypeMain bindKey:authKey];
|
||||
action.completedWithResult = ^(bool success) {
|
||||
MTDatacenterAuthAction *action = [[MTDatacenterAuthAction alloc] initWithAuthKeyInfoSelector:MTDatacenterAuthInfoSelectorEphemeralMain isCdn:false completion:^(__unused MTDatacenterAuthAction *action, bool success) {
|
||||
[subscriber putNext:@(!success)];
|
||||
[subscriber putCompletion];
|
||||
};
|
||||
[action execute:context datacenterId:datacenterId isCdn:false];
|
||||
}];
|
||||
[action execute:context datacenterId:datacenterId];
|
||||
|
||||
[disposable setDisposable:[[MTBlockDisposable alloc] initWithBlock:^{
|
||||
[action cancel];
|
||||
|
||||
@ -88,7 +88,7 @@
|
||||
[self fail];
|
||||
else
|
||||
{
|
||||
if ([context authInfoForDatacenterWithId:_targetDatacenterId] != nil)
|
||||
if ([context authInfoForDatacenterWithId:_targetDatacenterId selector:MTDatacenterAuthInfoSelectorPersistent] != nil)
|
||||
{
|
||||
_mtProto = [[MTProto alloc] initWithContext:context datacenterId:_targetDatacenterId usageCalculationInfo:nil requiredAuthToken:nil authTokenMasterDatacenterId:0];
|
||||
_mtProto.useTempAuthKeys = useTempAuthKeys;
|
||||
@ -118,11 +118,11 @@
|
||||
[_requestService addRequest:request];
|
||||
}
|
||||
else
|
||||
[context authInfoForDatacenterWithIdRequired:_targetDatacenterId isCdn:false];
|
||||
[context authInfoForDatacenterWithIdRequired:_targetDatacenterId isCdn:false selector:MTDatacenterAuthInfoSelectorPersistent];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)contextDatacenterAuthInfoUpdated:(MTContext *)context datacenterId:(NSInteger)datacenterId authInfo:(MTDatacenterAuthInfo *)__unused authInfo
|
||||
- (void)contextDatacenterAuthInfoUpdated:(MTContext *)context datacenterId:(NSInteger)datacenterId authInfo:(MTDatacenterAuthInfo *)__unused authInfo selector:(MTDatacenterAuthInfoSelector)selector
|
||||
{
|
||||
if (_context != context || !_awaitingAddresSetUpdate)
|
||||
return;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
52
submodules/MtProtoKit/Sources/MTProtoEngine.m
Normal file
52
submodules/MtProtoKit/Sources/MTProtoEngine.m
Normal file
@ -0,0 +1,52 @@
|
||||
#import <MtProtoKit/MTProtoEngine.h>
|
||||
|
||||
#import "Utils/MTQueueLocalObject.h"
|
||||
#import <MtProtoKit/MTQueue.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface MTProtoEngineImpl : NSObject {
|
||||
MTQueue *_queue;
|
||||
id<MTProtoPersistenceInterface> _persistenceInterface;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTProtoEngineImpl
|
||||
|
||||
- (instancetype)initWithQueue:(MTQueue *)queue persistenceInterface:(id<MTProtoPersistenceInterface>)persistenceInterface {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_queue = queue;
|
||||
_persistenceInterface = persistenceInterface;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface MTProtoEngine () {
|
||||
MTQueue *_queue;
|
||||
MTQueueLocalObject<MTProtoEngineImpl *> *_impl;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTProtoEngine
|
||||
|
||||
- (instancetype)initWithPersistenceInterface:(id<MTProtoPersistenceInterface>)persistenceInterface {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_queue = [[MTQueue alloc] init];
|
||||
__auto_type queue = _queue;
|
||||
_impl = [[MTQueueLocalObject<MTProtoEngineImpl
|
||||
*> alloc] initWithQueue:queue generator:^MTProtoEngineImpl *{
|
||||
return [[MTProtoEngineImpl alloc] initWithQueue:queue persistenceInterface:persistenceInterface];
|
||||
}];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
48
submodules/MtProtoKit/Sources/MTProtoInstance.m
Normal file
48
submodules/MtProtoKit/Sources/MTProtoInstance.m
Normal file
@ -0,0 +1,48 @@
|
||||
#import <MtProtoKit/MTProtoInstance.h>
|
||||
|
||||
#import <MtProtoKit/MTQueue.h>
|
||||
#import "Utils/MTQueueLocalObject.h"
|
||||
|
||||
@interface MTProtoInstanceImpl : NSObject {
|
||||
MTQueue *_queue;
|
||||
MTProtoEngine *_engine;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTProtoInstanceImpl
|
||||
|
||||
- (instancetype)initWithQueue:(MTQueue *)queue engine:(MTProtoEngine *)engine {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_queue = queue;
|
||||
_engine = engine;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface MTProtoInstance () {
|
||||
MTQueue *_queue;
|
||||
MTQueueLocalObject<MTProtoInstanceImpl *> *_impl;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTProtoInstance
|
||||
|
||||
- (instancetype)initWithEngine:(MTProtoEngine *)engine {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_queue = [[MTQueue alloc] init];
|
||||
__auto_type queue = _queue;
|
||||
_impl = [[MTQueueLocalObject<MTProtoInstanceImpl
|
||||
*> alloc] initWithQueue:queue generator:^MTProtoInstanceImpl *{
|
||||
return [[MTProtoInstanceImpl alloc] initWithQueue:queue engine:engine];
|
||||
}];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
@ -391,12 +391,12 @@
|
||||
return currentData;
|
||||
}
|
||||
|
||||
- (MTMessageTransaction *)mtProtoMessageTransaction:(MTProto *)mtProto
|
||||
- (MTMessageTransaction *)mtProtoMessageTransaction:(MTProto *)mtProto authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector sessionInfo:(MTSessionInfo *)sessionInfo
|
||||
{
|
||||
NSMutableArray *messages = nil;
|
||||
NSMutableDictionary *requestInternalIdToMessageInternalId = nil;
|
||||
|
||||
bool requestsWillInitializeApi = _apiEnvironment != nil && ![_apiEnvironment.apiInitializationHash isEqualToString:[_context authInfoForDatacenterWithId:mtProto.datacenterId].authKeyAttributes[@"apiInitializationHash"]];
|
||||
bool requestsWillInitializeApi = _apiEnvironment != nil && ![_apiEnvironment.apiInitializationHash isEqualToString:[_context authInfoForDatacenterWithId:mtProto.datacenterId selector:authInfoSelector].authKeyAttributes[@"apiInitializationHash"]];
|
||||
|
||||
CFAbsoluteTime currentTime = MTAbsoluteSystemTime();
|
||||
|
||||
@ -561,7 +561,7 @@
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)mtProto:(MTProto *)__unused mtProto receivedMessage:(MTIncomingMessage *)message
|
||||
- (void)mtProto:(MTProto *)__unused mtProto receivedMessage:(MTIncomingMessage *)message authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector
|
||||
{
|
||||
if ([message.body isKindOfClass:[MTRpcResultMessage class]])
|
||||
{
|
||||
@ -610,13 +610,13 @@
|
||||
{
|
||||
rpcError = [[MTRpcError alloc] initWithErrorCode:500 errorDescription:@"TL_PARSING_ERROR"];
|
||||
[_context performBatchUpdates:^{
|
||||
MTDatacenterAuthInfo *authInfo = [_context authInfoForDatacenterWithId:mtProto.datacenterId];
|
||||
MTDatacenterAuthInfo *authInfo = [_context authInfoForDatacenterWithId:mtProto.datacenterId selector:authInfoSelector];
|
||||
|
||||
NSMutableDictionary *authKeyAttributes = [[NSMutableDictionary alloc] initWithDictionary:authInfo.authKeyAttributes];
|
||||
authKeyAttributes[@"apiInitializationHash"] = @"";
|
||||
|
||||
authInfo = [authInfo withUpdatedAuthKeyAttributes:authKeyAttributes];
|
||||
[_context updateAuthInfoForDatacenterWithId:mtProto.datacenterId authInfo:authInfo];
|
||||
[_context updateAuthInfoForDatacenterWithId:mtProto.datacenterId authInfo:authInfo selector:authInfoSelector];
|
||||
}];
|
||||
}
|
||||
}
|
||||
@ -636,7 +636,7 @@
|
||||
|
||||
if (rpcResult != nil && request.requestContext.willInitializeApi)
|
||||
{
|
||||
MTDatacenterAuthInfo *authInfo = [_context authInfoForDatacenterWithId:mtProto.datacenterId];
|
||||
MTDatacenterAuthInfo *authInfo = [_context authInfoForDatacenterWithId:mtProto.datacenterId selector:authInfoSelector];
|
||||
|
||||
if (![_apiEnvironment.apiInitializationHash isEqualToString:authInfo.authKeyAttributes[@"apiInitializationHash"]])
|
||||
{
|
||||
@ -644,7 +644,7 @@
|
||||
authKeyAttributes[@"apiInitializationHash"] = _apiEnvironment.apiInitializationHash;
|
||||
|
||||
authInfo = [authInfo withUpdatedAuthKeyAttributes:authKeyAttributes];
|
||||
[_context updateAuthInfoForDatacenterWithId:mtProto.datacenterId authInfo:authInfo];
|
||||
[_context updateAuthInfoForDatacenterWithId:mtProto.datacenterId authInfo:authInfo selector:authInfoSelector];
|
||||
}
|
||||
}
|
||||
|
||||
@ -726,13 +726,13 @@
|
||||
{
|
||||
[_context performBatchUpdates:^
|
||||
{
|
||||
MTDatacenterAuthInfo *authInfo = [_context authInfoForDatacenterWithId:mtProto.datacenterId];
|
||||
MTDatacenterAuthInfo *authInfo = [_context authInfoForDatacenterWithId:mtProto.datacenterId selector:authInfoSelector];
|
||||
|
||||
NSMutableDictionary *authKeyAttributes = [[NSMutableDictionary alloc] initWithDictionary:authInfo.authKeyAttributes];
|
||||
[authKeyAttributes removeObjectForKey:@"apiInitializationHash"];
|
||||
|
||||
authInfo = [authInfo withUpdatedAuthKeyAttributes:authKeyAttributes];
|
||||
[_context updateAuthInfoForDatacenterWithId:mtProto.datacenterId authInfo:authInfo];
|
||||
[_context updateAuthInfoForDatacenterWithId:mtProto.datacenterId authInfo:authInfo selector:authInfoSelector];
|
||||
}];
|
||||
} else if (rpcError.errorCode == 406) {
|
||||
if (_didReceiveSoftAuthResetError) {
|
||||
|
||||
@ -41,7 +41,7 @@
|
||||
[mtProto requestTransportTransaction];
|
||||
}
|
||||
|
||||
- (MTMessageTransaction *)mtProtoMessageTransaction:(MTProto *)mtProto
|
||||
- (MTMessageTransaction *)mtProtoMessageTransaction:(MTProto *)mtProto authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector sessionInfo:(MTSessionInfo *)sessionInfo
|
||||
{
|
||||
if (_currentRequestMessageId == 0 || _currentRequestTransactionId == nil)
|
||||
{
|
||||
@ -121,7 +121,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)mtProto:(MTProto *)mtProto receivedMessage:(MTIncomingMessage *)message
|
||||
- (void)mtProto:(MTProto *)mtProto receivedMessage:(MTIncomingMessage *)message authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector
|
||||
{
|
||||
if (message.messageId == _messageId)
|
||||
{
|
||||
|
||||
@ -747,7 +747,7 @@ static const NSTimeInterval MTTcpTransportSleepWatchdogTimeout = 60.0;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)mtProto:(MTProto *)__unused mtProto receivedMessage:(MTIncomingMessage *)incomingMessage
|
||||
- (void)mtProto:(MTProto *)__unused mtProto receivedMessage:(MTIncomingMessage *)incomingMessage authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector
|
||||
{
|
||||
if ([incomingMessage.body isKindOfClass:[MTPongMessage class]])
|
||||
{
|
||||
|
||||
@ -45,7 +45,7 @@
|
||||
[mtProto requestTransportTransaction];
|
||||
}
|
||||
|
||||
- (MTMessageTransaction *)mtProtoMessageTransaction:(MTProto *)mtProto
|
||||
- (MTMessageTransaction *)mtProtoMessageTransaction:(MTProto *)mtProto authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector sessionInfo:(MTSessionInfo *)sessionInfo
|
||||
{
|
||||
if (_currentTransactionId == nil)
|
||||
{
|
||||
@ -127,7 +127,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)mtProto:(MTProto *)mtProto receivedMessage:(MTIncomingMessage *)message
|
||||
- (void)mtProto:(MTProto *)mtProto receivedMessage:(MTIncomingMessage *)message authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector
|
||||
{
|
||||
if ([message.body isKindOfClass:[MTFutureSaltsMessage class]] && ((MTFutureSaltsMessage *)message.body).requestMessageId == _currentMessageId)
|
||||
{
|
||||
|
||||
14
submodules/MtProtoKit/Sources/Utils/MTQueueLocalObject.h
Normal file
14
submodules/MtProtoKit/Sources/Utils/MTQueueLocalObject.h
Normal file
@ -0,0 +1,14 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class MTQueue;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface MTQueueLocalObject<__covariant T> : NSObject
|
||||
|
||||
- (instancetype)initWithQueue:(MTQueue *)queue generator:(T(^)())generator;
|
||||
- (void)with:(void (^)(T))f;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
56
submodules/MtProtoKit/Sources/Utils/MTQueueLocalObject.m
Normal file
56
submodules/MtProtoKit/Sources/Utils/MTQueueLocalObject.m
Normal file
@ -0,0 +1,56 @@
|
||||
#import "MTQueueLocalObject.h"
|
||||
#import <MtProtoKit/MTQueue.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface MTQueueLocalObjectHolder : NSObject
|
||||
|
||||
@property (nonatomic, assign) CFTypeRef impl;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTQueueLocalObjectHolder
|
||||
|
||||
@end
|
||||
|
||||
@interface MTQueueLocalObject () {
|
||||
MTQueue *_queue;
|
||||
MTQueueLocalObjectHolder *_holder;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTQueueLocalObject
|
||||
|
||||
- (instancetype)initWithQueue:(MTQueue *)queue generator:(id(^)())generator {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_queue = queue;
|
||||
_holder = [[MTQueueLocalObjectHolder alloc] init];
|
||||
__auto_type holder = _holder;
|
||||
[queue dispatchOnQueue:^{
|
||||
id value = generator();
|
||||
holder.impl = CFBridgingRetain(value);
|
||||
} synchronous:false];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
__auto_type holder = _holder;
|
||||
[_queue dispatchOnQueue:^{
|
||||
CFBridgingRelease(holder.impl);
|
||||
} synchronous:false];
|
||||
}
|
||||
|
||||
- (void)with:(void (^)(id))f {
|
||||
__auto_type holder = _holder;
|
||||
[_queue dispatchOnQueue:^{
|
||||
id value = (__bridge id)holder.impl;
|
||||
f(value);
|
||||
} synchronous:false];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@ -487,6 +487,8 @@ private func stringForRight(strings: PresentationStrings, right: TelegramChatAdm
|
||||
return strings.Channel_EditAdmin_PermissionPinMessages
|
||||
} else if right.contains(.canAddAdmins) {
|
||||
return strings.Channel_EditAdmin_PermissionAddAdmins
|
||||
} else if right.contains(.canBeAnonymous) {
|
||||
return strings.Channel_AdminLog_CanBeAnonymous
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
@ -509,6 +511,8 @@ private func rightDependencies(_ right: TelegramChatAdminRightsFlags) -> [Telegr
|
||||
return []
|
||||
} else if right.contains(.canAddAdmins) {
|
||||
return []
|
||||
} else if right.contains(.canBeAnonymous) {
|
||||
return []
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
@ -607,11 +611,43 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s
|
||||
.canBanUsers,
|
||||
.canInviteUsers,
|
||||
.canPinMessages,
|
||||
.canBeAnonymous,
|
||||
.canAddAdmins
|
||||
]
|
||||
}
|
||||
|
||||
if isCreator {
|
||||
if isGroup {
|
||||
entries.append(.rightsTitle(presentationData.theme, presentationData.strings.Channel_EditAdmin_PermissionsHeader))
|
||||
|
||||
let accountUserRightsFlags: TelegramChatAdminRightsFlags
|
||||
if channel.flags.contains(.isCreator) {
|
||||
accountUserRightsFlags = maskRightsFlags
|
||||
} else if let adminRights = channel.adminRights {
|
||||
accountUserRightsFlags = maskRightsFlags.intersection(adminRights.flags)
|
||||
} else {
|
||||
accountUserRightsFlags = []
|
||||
}
|
||||
|
||||
let currentRightsFlags: TelegramChatAdminRightsFlags
|
||||
if let updatedFlags = state.updatedFlags {
|
||||
currentRightsFlags = updatedFlags
|
||||
} else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminRights, _, _) = initialParticipant, let adminRights = maybeAdminRights {
|
||||
currentRightsFlags = adminRights.rights.flags
|
||||
} else if let initialParticipant = initialParticipant, case let .creator(_, maybeAdminRights, _) = initialParticipant, let adminRights = maybeAdminRights {
|
||||
currentRightsFlags = adminRights.rights.flags
|
||||
} else {
|
||||
currentRightsFlags = accountUserRightsFlags.subtracting(.canAddAdmins).subtracting(.canBeAnonymous)
|
||||
}
|
||||
|
||||
var index = 0
|
||||
for right in rightsOrder {
|
||||
if accountUserRightsFlags.contains(right) {
|
||||
entries.append(.rightItem(presentationData.theme, index, stringForRight(strings: presentationData.strings, right: right, isGroup: isGroup, defaultBannedRights: channel.defaultBannedRights), right, currentRightsFlags, currentRightsFlags.contains(right), right == .canBeAnonymous))
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
entries.append(.rightsTitle(presentationData.theme, presentationData.strings.Channel_EditAdmin_PermissionsHeader))
|
||||
|
||||
@ -631,7 +667,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s
|
||||
} else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminRights, _, _) = initialParticipant, let adminRights = maybeAdminRights {
|
||||
currentRightsFlags = adminRights.rights.flags
|
||||
} else {
|
||||
currentRightsFlags = accountUserRightsFlags.subtracting(.canAddAdmins)
|
||||
currentRightsFlags = accountUserRightsFlags.subtracting(.canAddAdmins).subtracting(.canBeAnonymous)
|
||||
}
|
||||
|
||||
var index = 0
|
||||
@ -731,6 +767,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s
|
||||
.canBanUsers,
|
||||
.canInviteUsers,
|
||||
.canPinMessages,
|
||||
.canBeAnonymous,
|
||||
.canAddAdmins
|
||||
]
|
||||
|
||||
|
||||
@ -696,7 +696,7 @@ public func channelAdminsController(context: AccountContext, peerId initialPeerI
|
||||
if let peer = peerView.peers[participant.peerId] {
|
||||
switch participant {
|
||||
case .creator:
|
||||
result.append(RenderedChannelParticipant(participant: .creator(id: peer.id, rank: nil), peer: peer))
|
||||
result.append(RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: nil), peer: peer))
|
||||
case .admin:
|
||||
var peers: [PeerId: Peer] = [:]
|
||||
peers[creator.id] = creator
|
||||
|
||||
@ -166,7 +166,7 @@ private func channelDiscussionGroupSetupControllerEntries(presentationData: Pres
|
||||
|
||||
var entries: [ChannelDiscussionGroupSetupControllerEntry] = []
|
||||
|
||||
if let linkedDiscussionPeerId = cachedData.linkedDiscussionPeerId {
|
||||
if case let .known(maybeLinkedDiscussionPeerId) = cachedData.linkedDiscussionPeerId, let linkedDiscussionPeerId = maybeLinkedDiscussionPeerId {
|
||||
if let group = view.peers[linkedDiscussionPeerId] {
|
||||
if case .group = peer.info {
|
||||
entries.append(.header(presentationData.theme, presentationData.strings, group.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), true, presentationData.strings.Channel_DiscussionGroup_HeaderLabel))
|
||||
@ -299,7 +299,7 @@ public func channelDiscussionGroupSetupController(context: AccountContext, peerI
|
||||
return
|
||||
}
|
||||
|
||||
if groupId == cachedData.linkedDiscussionPeerId {
|
||||
if case let .known(maybeLinkedDiscussionPeerId) = cachedData.linkedDiscussionPeerId, maybeLinkedDiscussionPeerId == groupId {
|
||||
navigateToGroupImpl?(groupId)
|
||||
return
|
||||
}
|
||||
@ -483,7 +483,7 @@ public func channelDiscussionGroupSetupController(context: AccountContext, peerI
|
||||
let applyPeerId: PeerId
|
||||
if case .broadcast = peer.info {
|
||||
applyPeerId = peerId
|
||||
} else if let linkedDiscussionPeerId = cachedData.linkedDiscussionPeerId {
|
||||
} else if case let .known(maybeLinkedDiscussionPeerId) = cachedData.linkedDiscussionPeerId, let linkedDiscussionPeerId = maybeLinkedDiscussionPeerId {
|
||||
applyPeerId = linkedDiscussionPeerId
|
||||
} else {
|
||||
return
|
||||
|
||||
@ -497,7 +497,7 @@ private func channelInfoEntries(account: Account, presentationData: Presentation
|
||||
|
||||
let discussionGroupTitle: String
|
||||
if let cachedData = view.cachedData as? CachedChannelData {
|
||||
if let linkedDiscussionPeerId = cachedData.linkedDiscussionPeerId, let peer = view.peers[linkedDiscussionPeerId] {
|
||||
if case let .known(maybeLinkedDiscussionPeerId) = cachedData.linkedDiscussionPeerId, let linkedDiscussionPeerId = maybeLinkedDiscussionPeerId, let peer = view.peers[linkedDiscussionPeerId] {
|
||||
if let addressName = peer.addressName, !addressName.isEmpty {
|
||||
discussionGroupTitle = "@\(addressName)"
|
||||
} else {
|
||||
@ -532,7 +532,7 @@ private func channelInfoEntries(account: Account, presentationData: Presentation
|
||||
if let _ = state.editingState, let adminRights = peer.adminRights, !adminRights.isEmpty {
|
||||
let discussionGroupTitle: String?
|
||||
if let cachedData = view.cachedData as? CachedChannelData {
|
||||
if let linkedDiscussionPeerId = cachedData.linkedDiscussionPeerId, let peer = view.peers[linkedDiscussionPeerId] {
|
||||
if case let .known(maybeLinkedDiscussionPeerId) = cachedData.linkedDiscussionPeerId, let linkedDiscussionPeerId = maybeLinkedDiscussionPeerId, let peer = view.peers[linkedDiscussionPeerId] {
|
||||
if let addressName = peer.addressName, !addressName.isEmpty {
|
||||
discussionGroupTitle = "@\(addressName)"
|
||||
} else {
|
||||
@ -951,7 +951,7 @@ public func channelInfoController(context: AccountContext, peerId: PeerId) -> Vi
|
||||
if canEditChannel {
|
||||
hasSomethingToEdit = true
|
||||
} else if let adminRights = peer.adminRights, !adminRights.isEmpty {
|
||||
if let cachedData = view.cachedData as? CachedChannelData, let _ = cachedData.linkedDiscussionPeerId {
|
||||
if let cachedData = view.cachedData as? CachedChannelData, case let .known(maybeLinkedDiscussionPeerId) = cachedData.linkedDiscussionPeerId, let _ = maybeLinkedDiscussionPeerId {
|
||||
hasSomethingToEdit = true
|
||||
}
|
||||
}
|
||||
|
||||
@ -867,7 +867,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon
|
||||
let renderedParticipant: RenderedChannelParticipant
|
||||
switch participant {
|
||||
case .creator:
|
||||
renderedParticipant = RenderedChannelParticipant(participant: .creator(id: peer.id, rank: nil), peer: peer)
|
||||
renderedParticipant = RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: nil), peer: peer)
|
||||
case .admin:
|
||||
var peers: [PeerId: Peer] = [:]
|
||||
if let creator = creatorPeer {
|
||||
|
||||
@ -216,7 +216,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode {
|
||||
let renderedParticipant: RenderedChannelParticipant
|
||||
switch participant {
|
||||
case .creator:
|
||||
renderedParticipant = RenderedChannelParticipant(participant: .creator(id: peer.id, rank: nil), peer: peer)
|
||||
renderedParticipant = RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: nil), peer: peer)
|
||||
case .admin:
|
||||
var peers: [PeerId: Peer] = [:]
|
||||
peers[creator.id] = creator
|
||||
|
||||
@ -896,7 +896,7 @@ private func groupInfoEntries(account: Account, presentationData: PresentationDa
|
||||
} else {
|
||||
if cachedChannelData.flags.contains(.canChangeUsername) {
|
||||
entries.append(GroupInfoEntry.groupTypeSetup(presentationData.theme, presentationData.strings.GroupInfo_GroupType, isPublic ? presentationData.strings.Channel_Setup_TypePublic : presentationData.strings.Channel_Setup_TypePrivate))
|
||||
if let linkedDiscussionPeerId = cachedChannelData.linkedDiscussionPeerId, let peer = view.peers[linkedDiscussionPeerId] {
|
||||
if case let .known(maybeLinkedDiscussionPeerId) = cachedChannelData.linkedDiscussionPeerId, let linkedDiscussionPeerId = maybeLinkedDiscussionPeerId, let peer = view.peers[linkedDiscussionPeerId] {
|
||||
let peerTitle: String
|
||||
if let addressName = peer.addressName, !addressName.isEmpty {
|
||||
peerTitle = "@\(addressName)"
|
||||
@ -1069,7 +1069,7 @@ private func groupInfoEntries(account: Account, presentationData: PresentationDa
|
||||
let participant: ChannelParticipant
|
||||
switch sortedParticipants[i] {
|
||||
case .creator:
|
||||
participant = .creator(id: sortedParticipants[i].peerId, rank: nil)
|
||||
participant = .creator(id: sortedParticipants[i].peerId, adminInfo: nil, rank: nil)
|
||||
memberStatus = .owner(rank: nil)
|
||||
case .admin:
|
||||
participant = .member(id: sortedParticipants[i].peerId, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(flags: .groupSpecific), promotedBy: account.peerId, canBeEditedByAccountPeer: true), banInfo: nil, rank: nil)
|
||||
@ -1201,7 +1201,7 @@ private func groupInfoEntries(account: Account, presentationData: PresentationDa
|
||||
let participant = participants[i]
|
||||
let memberStatus: GroupInfoMemberStatus
|
||||
switch participant.participant {
|
||||
case let .creator(_, rank):
|
||||
case let .creator(_, _, rank):
|
||||
memberStatus = .owner(rank: rank)
|
||||
case let .member(_, _, adminInfo, _, rank):
|
||||
if adminInfo != nil {
|
||||
|
||||
@ -10,6 +10,7 @@ public enum AdditionalMessageHistoryViewData {
|
||||
case preferencesEntry(ValueBoxKey)
|
||||
case peer(PeerId)
|
||||
case peerIsContact(PeerId)
|
||||
case message(MessageId)
|
||||
}
|
||||
|
||||
public enum AdditionalMessageHistoryViewDataEntry {
|
||||
@ -22,4 +23,5 @@ public enum AdditionalMessageHistoryViewDataEntry {
|
||||
case preferencesEntry(ValueBoxKey, PreferencesEntry?)
|
||||
case peerIsContact(PeerId, Bool)
|
||||
case peer(PeerId, Peer?)
|
||||
case message(MessageId, Message?)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user