Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2022-11-26 16:33:00 +04:00
commit 12c0603ac4
88 changed files with 2346 additions and 814 deletions

View File

@ -36,7 +36,7 @@ func unreadMessages(account: Account) -> Signal<[INMessage], NoError> {
|> mapToSignal { view -> Signal<[INMessage], NoError> in
var signals: [Signal<[INMessage], NoError>] = []
for entry in view.0.entries {
if case let .MessageEntry(index, _, readState, isMuted, _, _, _, _, _, _, _, _) = entry {
if case let .MessageEntry(index, _, readState, isMuted, _, _, _, _, _, _, _, _, _) = entry {
if index.messageIndex.id.peerId.namespace != Namespaces.Peer.CloudUser {
continue
}

View File

@ -8318,6 +8318,8 @@ Sorry for the inconvenience.";
"Stickers.EmojiPackInfoText" = "This message contains **%@** emoji.";
"PeerInfo.TopicIconInfoText" = "This topic's icon is from **%@**.";
"AutoremoveSetup.AdditionalGlobalSettingsInfo" = "You can also set your default [self-destruct timer]() for all chats in Settings.";
"Login.EnterCodeFragmentTitle" = "Enter Code";
"Login.EnterCodeFragmentText" = "Check the Anonymous Numbers section on **Fragment** to get the code.";
"Login.OpenFragment" = "Open Fragment";

View File

@ -179,6 +179,8 @@ public enum WallpaperUrlParameter {
public enum ResolvedUrlSettingsSection {
case theme
case devices
case autoremoveMessages
case twoStepAuth
}
public struct ResolvedBotChoosePeerTypes: OptionSet {
@ -373,6 +375,17 @@ public enum ChatLocation: Equatable {
case feed(id: Int32)
}
public extension ChatLocation {
var normalized: ChatLocation {
switch self {
case .peer, .feed:
return self
case let .replyThread(message):
return .replyThread(message: message.normalized)
}
}
}
public enum ChatControllerActivateInput {
case text
case entityInput

View File

@ -36,10 +36,35 @@ public struct ContactMultiselectionControllerAdditionalCategories {
}
public enum ContactMultiselectionControllerMode {
public struct ChatSelection {
public var title: String
public var searchPlaceholder: String
public var selectedChats: Set<PeerId>
public var additionalCategories: ContactMultiselectionControllerAdditionalCategories?
public var chatListFilters: [ChatListFilter]?
public var displayAutoremoveTimeout: Bool
public init(
title: String,
searchPlaceholder: String,
selectedChats: Set<PeerId>,
additionalCategories: ContactMultiselectionControllerAdditionalCategories?,
chatListFilters: [ChatListFilter]?,
displayAutoremoveTimeout: Bool = false
) {
self.title = title
self.searchPlaceholder = searchPlaceholder
self.selectedChats = selectedChats
self.additionalCategories = additionalCategories
self.chatListFilters = chatListFilters
self.displayAutoremoveTimeout = displayAutoremoveTimeout
}
}
case groupCreation
case peerSelection(searchChatList: Bool, searchGroups: Bool, searchChannels: Bool)
case channelCreation
case chatSelection(title: String, selectedChats: Set<PeerId>, additionalCategories: ContactMultiselectionControllerAdditionalCategories?, chatListFilters: [ChatListFilter]?)
case chatSelection(ChatSelection)
}
public enum ContactListFilter {
@ -54,16 +79,18 @@ public final class ContactMultiselectionControllerParams {
public let mode: ContactMultiselectionControllerMode
public let options: [ContactListAdditionalOption]
public let filters: [ContactListFilter]
public let isPeerEnabled: ((EnginePeer) -> Bool)?
public let alwaysEnabled: Bool
public let limit: Int32?
public let reachedLimit: ((Int32) -> Void)?
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, mode: ContactMultiselectionControllerMode, options: [ContactListAdditionalOption], filters: [ContactListFilter] = [.excludeSelf], alwaysEnabled: Bool = false, limit: Int32? = nil, reachedLimit: ((Int32) -> Void)? = nil) {
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, mode: ContactMultiselectionControllerMode, options: [ContactListAdditionalOption], filters: [ContactListFilter] = [.excludeSelf], isPeerEnabled: ((EnginePeer) -> Bool)? = nil, alwaysEnabled: Bool = false, limit: Int32? = nil, reachedLimit: ((Int32) -> Void)? = nil) {
self.context = context
self.updatedPresentationData = updatedPresentationData
self.mode = mode
self.options = options
self.filters = filters
self.isPeerEnabled = isPeerEnabled
self.alwaysEnabled = alwaysEnabled
self.limit = limit
self.reachedLimit = reachedLimit

View File

@ -1254,7 +1254,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
chatController.canReadHistory.set(false)
source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController))
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: source, items: chatForumTopicMenuItems(context: strongSelf.context, peerId: peer.peerId, threadId: threadId, isPinned: nil, isClosed: nil, chatListController: strongSelf, joined: joined, canSelect: true) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: source, items: chatForumTopicMenuItems(context: strongSelf.context, peerId: peer.peerId, threadId: threadId, isPinned: nil, isClosed: nil, chatListController: strongSelf, joined: joined, canSelect: false) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
strongSelf.presentInGlobalOverlay(contextController)
} else {
let chatListController = ChatListControllerImpl(context: strongSelf.context, location: .forum(peerId: channel.id), controlsHistoryPreload: false, hideNetworkActivityStatus: true, previewing: true, enableDebugActions: false)
@ -1302,21 +1302,28 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return
}
let contextContentSource: ContextContentSource
if peer.id.namespace == Namespaces.Peer.SecretChat, let node = node.subnodes?.first as? ContextExtractedContentContainingNode {
contextContentSource = .extracted(ChatListHeaderBarContextExtractedContentSource(controller: strongSelf, sourceNode: node, keepInPlace: false))
if case let .channel(channel) = peer, channel.flags.contains(.isForum) {
let chatListController = ChatListControllerImpl(context: strongSelf.context, location: .forum(peerId: channel.id), controlsHistoryPreload: false, hideNetworkActivityStatus: true, previewing: true, enableDebugActions: false)
chatListController.navigationPresentation = .master
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatListController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: nil, source: .search(source), chatListController: strongSelf, joined: false) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
strongSelf.presentInGlobalOverlay(contextController)
} else {
var subject: ChatControllerSubject?
if case let .search(messageId) = source, let id = messageId {
subject = .message(id: .id(id), highlight: false, timecode: nil)
let contextContentSource: ContextContentSource
if peer.id.namespace == Namespaces.Peer.SecretChat, let node = node.subnodes?.first as? ContextExtractedContentContainingNode {
contextContentSource = .extracted(ChatListHeaderBarContextExtractedContentSource(controller: strongSelf, sourceNode: node, keepInPlace: false))
} else {
var subject: ChatControllerSubject?
if case let .search(messageId) = source, let id = messageId {
subject = .message(id: .id(id), highlight: false, timecode: nil)
}
let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peer.id), subject: subject, botStart: nil, mode: .standard(previewing: true))
chatController.canReadHistory.set(false)
contextContentSource = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController))
}
let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peer.id), subject: subject, botStart: nil, mode: .standard(previewing: true))
chatController.canReadHistory.set(false)
contextContentSource = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController))
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: contextContentSource, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: nil, source: .search(source), chatListController: strongSelf, joined: false) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
strongSelf.presentInGlobalOverlay(contextController)
}
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: contextContentSource, items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: nil, source: .search(source), chatListController: strongSelf, joined: false) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
strongSelf.presentInGlobalOverlay(contextController)
}
self.tabContainerNode.tabSelected = { [weak self] id, isDisabled in
@ -2048,8 +2055,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let navigationBarHeight = self.navigationBar?.frame.maxY ?? 0.0
transition.updateAlpha(node: self.tabContainerNode, alpha: self.chatListDisplayNode.inlineStackContainerTransitionFraction * 0.5 + (1.0 - self.chatListDisplayNode.inlineStackContainerTransitionFraction) * 1.0)
self.tabContainerNode.isUserInteractionEnabled = self.chatListDisplayNode.inlineStackContainerNode == nil
//transition.updateAlpha(node: self.tabContainerNode, alpha: self.chatListDisplayNode.inlineStackContainerTransitionFraction * 0.5 + (1.0 - self.chatListDisplayNode.inlineStackContainerTransitionFraction) * 1.0)
//self.tabContainerNode.isUserInteractionEnabled = self.chatListDisplayNode.inlineStackContainerNode == nil
transition.updateFrame(node: self.tabContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight - self.additionalNavigationBarHeight - 46.0 + tabContainerOffset), size: CGSize(width: layout.size.width, height: 46.0)))
self.tabContainerNode.update(size: CGSize(width: layout.size.width, height: 46.0), sideInset: layout.safeInsets.left, filters: self.tabContainerData?.0 ?? [], selectedFilter: self.chatListDisplayNode.mainContainerNode.currentItemFilter, isReordering: self.chatListDisplayNode.isReorderingFilters || (self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing && !self.chatListDisplayNode.didBeginSelectingChatsWhileEditing), isEditing: self.chatListDisplayNode.effectiveContainerNode.currentItemNode.currentState.editing, canReorderAllChats: self.isPremium, filtersLimit: self.tabContainerData?.2, transitionFraction: self.chatListDisplayNode.effectiveContainerNode.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring))
@ -4689,12 +4696,6 @@ private final class ChatListLocationContext {
longTapped: {
}
)
self.rightButton = AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent(
content: .text(title: presentationData.strings.Common_Done, isBold: true),
pressed: { [weak self] _ in
self?.parentController?.donePressed()
}
)))
} else {
self.chatTitleComponent = ChatTitleComponent(
context: self.context,
@ -4724,7 +4725,16 @@ private final class ChatListLocationContext {
self.parentController?.activateSearch()
}
)
}
if stateAndFilterId.state.editing {
self.rightButton = AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent(
content: .text(title: presentationData.strings.Common_Done, isBold: true),
pressed: { [weak self] _ in
self?.parentController?.donePressed()
}
)))
} else {
self.rightButton = AnyComponentWithIdentity(id: "more", component: AnyComponent(NavigationButtonComponent(
content: .more,
pressed: { [weak self] sourceView in

View File

@ -601,7 +601,13 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
var pushImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .chatSelection(title: presentationData.strings.ChatListFolder_IncludeChatsTitle, selectedChats: Set(filterData.includePeers.peers), additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories), chatListFilters: allFilters), options: [], filters: [], alwaysEnabled: true, limit: isPremium ? premiumLimit : limit, reachedLimit: { count in
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .chatSelection(ContactMultiselectionControllerMode.ChatSelection(
title: presentationData.strings.ChatListFolder_IncludeChatsTitle,
searchPlaceholder: presentationData.strings.ChatListFilter_AddChatsTitle,
selectedChats: Set(filterData.includePeers.peers),
additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories),
chatListFilters: allFilters
)), options: [], filters: [], alwaysEnabled: true, limit: isPremium ? premiumLimit : limit, reachedLimit: { count in
if count >= premiumLimit {
let limitController = PremiumLimitScreen(context: context, subject: .chatsPerFolder, count: min(premiumLimit, count), action: {})
pushImpl?(limitController)
@ -723,7 +729,13 @@ private func internalChatListFilterExcludeChatsController(context: AccountContex
selectedCategories.insert(AdditionalExcludeCategoryId.archived.rawValue)
}
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .chatSelection(title: presentationData.strings.ChatListFolder_ExcludeChatsTitle, selectedChats: Set(filterData.excludePeers), additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories), chatListFilters: allFilters), options: [], filters: [], alwaysEnabled: true, limit: 100))
let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .chatSelection(ContactMultiselectionControllerMode.ChatSelection(
title: presentationData.strings.ChatListFolder_ExcludeChatsTitle,
searchPlaceholder: presentationData.strings.ChatListFilter_AddChatsTitle,
selectedChats: Set(filterData.excludePeers),
additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories),
chatListFilters: allFilters
)), options: [], filters: [], alwaysEnabled: true, limit: 100))
controller.navigationPresentation = .modal
let _ = (controller.result
|> take(1)

View File

@ -157,9 +157,9 @@ private enum ChatListRecentEntry: Comparable, Identifiable {
} else if case let .user(user) = primaryPeer {
let servicePeer = isServicePeer(primaryPeer._asPeer())
if user.flags.contains(.isSupport) && !servicePeer {
status = .custom(string: strings.Bot_GenericSupportStatus, multiline: false)
status = .custom(string: strings.Bot_GenericSupportStatus, multiline: false, isActive: false, icon: nil)
} else if let _ = user.botInfo {
status = .custom(string: strings.Bot_GenericBotStatus, multiline: false)
status = .custom(string: strings.Bot_GenericBotStatus, multiline: false, isActive: false, icon: nil)
} else if user.id != context.account.peerId && !servicePeer {
let presence = peer.presence ?? TelegramUserPresence(status: .none, lastActivity: 0)
status = .presence(EnginePeer.Presence(presence), timeFormat)
@ -167,19 +167,19 @@ private enum ChatListRecentEntry: Comparable, Identifiable {
status = .none
}
} else if case let .legacyGroup(group) = primaryPeer {
status = .custom(string: strings.GroupInfo_ParticipantCount(Int32(group.participantCount)), multiline: false)
status = .custom(string: strings.GroupInfo_ParticipantCount(Int32(group.participantCount)), multiline: false, isActive: false, icon: nil)
} else if case let .channel(channel) = primaryPeer {
if case .group = channel.info {
if let count = peer.subpeerSummary?.count {
status = .custom(string: strings.GroupInfo_ParticipantCount(Int32(count)), multiline: false)
status = .custom(string: strings.GroupInfo_ParticipantCount(Int32(count)), multiline: false, isActive: false, icon: nil)
} else {
status = .custom(string: strings.Group_Status, multiline: false)
status = .custom(string: strings.Group_Status, multiline: false, isActive: false, icon: nil)
}
} else {
if let count = peer.subpeerSummary?.count {
status = .custom(string: strings.Conversation_StatusSubscribers(Int32(count)), multiline: false)
status = .custom(string: strings.Conversation_StatusSubscribers(Int32(count)), multiline: false, isActive: false, icon: nil)
} else {
status = .custom(string: strings.Channel_Status, multiline: false)
status = .custom(string: strings.Channel_Status, multiline: false, isActive: false, icon: nil)
}
}
} else {
@ -925,7 +925,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
private let presentationDataPromise = Promise<ChatListPresentationData>()
private var searchStateValue = ChatListSearchListPaneNodeState()
private let searchStatePromise = ValuePromise<ChatListSearchListPaneNodeState>()
private let searchContextValue = Atomic<ChatListSearchMessagesContext?>(value: nil)
private let searchContextsValue = Atomic<[Int: ChatListSearchMessagesContext]>(value: [:])
var searchCurrentMessages: [EngineMessage]?
var currentEntries: [ChatListSearchEntry]?
@ -1081,11 +1081,11 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
self.addSubnode(self.shimmerNode)
self.addSubnode(self.mediaAccessoryPanelContainer)
let searchContext = Promise<ChatListSearchMessagesContext?>(nil)
let searchContextValue = self.searchContextValue
let updateSearchContext: ((ChatListSearchMessagesContext?) -> (ChatListSearchMessagesContext?, Bool)) -> Void = { f in
let searchContexts = Promise<[Int: ChatListSearchMessagesContext]>([:])
let searchContextsValue = self.searchContextsValue
let updateSearchContexts: (([Int: ChatListSearchMessagesContext]) -> ([Int: ChatListSearchMessagesContext], Bool)) -> Void = { f in
var shouldUpdate = false
let updated = searchContextValue.modify { current in
let updated = searchContextsValue.modify { current in
let (u, s) = f(current)
shouldUpdate = s
if s {
@ -1095,7 +1095,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}
}
if shouldUpdate {
searchContext.set(.single(updated))
searchContexts.set(.single(updated))
}
}
@ -1419,32 +1419,32 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
} else {
foundRemotePeers = .single(([], [], false))
}
let searchLocation: SearchMessagesLocation
let searchLocations: [SearchMessagesLocation]
if let options = options {
if case let .forum(peerId) = location {
searchLocation = .peer(peerId: peerId, fromId: nil, tags: tagMask, topMsgId: nil, minDate: options.date?.0, maxDate: options.date?.1)
searchLocations = [.peer(peerId: peerId, fromId: nil, tags: tagMask, topMsgId: nil, minDate: options.date?.0, maxDate: options.date?.1), .general(tags: tagMask, minDate: options.date?.0, maxDate: options.date?.1)]
} else if let (peerId, _, _) = options.peer {
searchLocation = .peer(peerId: peerId, fromId: nil, tags: tagMask, topMsgId: nil, minDate: options.date?.0, maxDate: options.date?.1)
searchLocations = [.peer(peerId: peerId, fromId: nil, tags: tagMask, topMsgId: nil, minDate: options.date?.0, maxDate: options.date?.1)]
} else {
if case let .chatList(groupId) = location, case .archive = groupId {
searchLocation = .group(groupId: groupId._asGroup(), tags: tagMask, minDate: options.date?.0, maxDate: options.date?.1)
searchLocations = [.group(groupId: groupId._asGroup(), tags: tagMask, minDate: options.date?.0, maxDate: options.date?.1)]
} else {
searchLocation = .general(tags: tagMask, minDate: options.date?.0, maxDate: options.date?.1)
searchLocations = [.general(tags: tagMask, minDate: options.date?.0, maxDate: options.date?.1)]
}
}
} else {
if case let .forum(peerId) = location {
searchLocation = .peer(peerId: peerId, fromId: nil, tags: tagMask, topMsgId: nil, minDate: nil, maxDate: nil)
searchLocations = [.peer(peerId: peerId, fromId: nil, tags: tagMask, topMsgId: nil, minDate: nil, maxDate: nil), .general(tags: tagMask, minDate: nil, maxDate: nil)]
} else if case let .chatList(groupId) = location, case .archive = groupId {
searchLocation = .group(groupId: groupId._asGroup(), tags: tagMask, minDate: nil, maxDate: nil)
searchLocations = [.group(groupId: groupId._asGroup(), tags: tagMask, minDate: nil, maxDate: nil)]
} else {
searchLocation = .general(tags: tagMask, minDate: nil, maxDate: nil)
searchLocations = [.general(tags: tagMask, minDate: nil, maxDate: nil)]
}
}
let finalQuery = query ?? ""
updateSearchContext { _ in
return (nil, true)
updateSearchContexts { _ in
return ([:], true)
}
let foundRemoteMessages: Signal<(([EngineMessage], [EnginePeer.Id: EnginePeerReadCounters], [EngineMessage.Id: MessageHistoryThreadData], Int32), Bool), NoError>
if peersFilter.contains(.doNotSearchMessages) {
@ -1454,23 +1454,28 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
addAppLogEvent(postbox: context.account.postbox, type: "search_global_query")
}
let searchSignal = context.engine.messages.searchMessages(location: searchLocation, query: finalQuery, state: nil, limit: 50)
|> map { result, updatedState -> ChatListSearchMessagesResult in
let searchSignals: [Signal<(SearchMessagesResult, SearchMessagesState), NoError>] = searchLocations.map { searchLocation in
return context.engine.messages.searchMessages(location: searchLocation, query: finalQuery, state: nil, limit: 50)
}
let searchSignal = combineLatest(searchSignals)
|> map { results -> ChatListSearchMessagesResult in
let (result, updatedState) = results[0]
return ChatListSearchMessagesResult(query: finalQuery, messages: result.messages.map({ EngineMessage($0) }).sorted(by: { $0.index > $1.index }), readStates: result.readStates.mapValues { EnginePeerReadCounters(state: $0, isMuted: false) }, threadInfo: result.threadInfo, hasMore: !result.completed, totalCount: result.totalCount, state: updatedState)
}
let loadMore = searchContext.get()
|> mapToSignal { searchContext -> Signal<(([EngineMessage], [EnginePeer.Id: EnginePeerReadCounters], [EngineMessage.Id: MessageHistoryThreadData], Int32), Bool), NoError> in
if let searchContext = searchContext, searchContext.result.hasMore {
let loadMore = searchContexts.get()
|> mapToSignal { searchContexts -> Signal<(([EngineMessage], [EnginePeer.Id: EnginePeerReadCounters], [EngineMessage.Id: MessageHistoryThreadData], Int32), Bool), NoError> in
if let searchContext = searchContexts[0], searchContext.result.hasMore {
if let _ = searchContext.loadMoreIndex {
return context.engine.messages.searchMessages(location: searchLocation, query: finalQuery, state: searchContext.result.state, limit: 80)
return context.engine.messages.searchMessages(location: searchLocations[0], query: finalQuery, state: searchContext.result.state, limit: 80)
|> map { result, updatedState -> ChatListSearchMessagesResult in
return ChatListSearchMessagesResult(query: finalQuery, messages: result.messages.map({ EngineMessage($0) }).sorted(by: { $0.index > $1.index }), readStates: result.readStates.mapValues { EnginePeerReadCounters(state: $0, isMuted: false) }, threadInfo: result.threadInfo, hasMore: !result.completed, totalCount: result.totalCount, state: updatedState)
}
|> mapToSignal { foundMessages -> Signal<(([EngineMessage], [EnginePeer.Id: EnginePeerReadCounters], [EngineMessage.Id: MessageHistoryThreadData], Int32), Bool), NoError> in
updateSearchContext { previous in
updateSearchContexts { previous in
let updated = ChatListSearchMessagesContext(result: foundMessages, loadMoreIndex: nil)
return (updated, true)
return ([0: updated], true)
}
return .complete()
}
@ -1486,8 +1491,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|> then(
searchSignal
|> map { foundMessages -> (([EngineMessage], [EnginePeer.Id: EnginePeerReadCounters], [EngineMessage.Id: MessageHistoryThreadData], Int32), Bool) in
updateSearchContext { _ in
return (ChatListSearchMessagesContext(result: foundMessages, loadMoreIndex: nil), true)
updateSearchContexts { _ in
return ([0: ChatListSearchMessagesContext(result: foundMessages, loadMoreIndex: nil)], true)
}
return ((foundMessages.messages, foundMessages.readStates, foundMessages.threadInfo, foundMessages.totalCount), false)
}
@ -1510,7 +1515,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
})
let foundThreads: Signal<[EngineChatList.Item], NoError>
if case .forum = location {
if case .forum = location, (key == .topics || key == .chats) {
foundThreads = chatListViewForLocation(chatListLocation: location, location: .initial(count: 1000, filter: nil), account: context.account)
|> map { view -> [EngineChatList.Item] in
var filteredItems: [EngineChatList.Item] = []
@ -1773,7 +1778,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}
}
let foundMessages = searchContext.get() |> map { searchContext -> ([EngineMessage], Int32, Bool) in
let foundMessages = searchContexts.get() |> map { searchContexts -> ([EngineMessage], Int32, Bool) in
let searchContext = searchContexts[0]
if let result = searchContext?.result {
return (result.messages, result.totalCount, result.hasMore)
} else {
@ -1782,17 +1788,17 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}
let loadMore = {
updateSearchContext { previous in
guard let previous = previous else {
return (nil, false)
updateSearchContexts { previousMap in
guard let previous = previousMap[0] else {
return ([:], false)
}
if previous.loadMoreIndex != nil {
return (previous, false)
return ([0: previous], false)
}
guard let last = previous.result.messages.last else {
return (previous, false)
return ([0: previous], false)
}
return (ChatListSearchMessagesContext(result: previous.result, loadMoreIndex: last.index), true)
return ([0: ChatListSearchMessagesContext(result: previous.result, loadMoreIndex: last.index)], true)
}
}
@ -3302,7 +3308,7 @@ private final class ChatListSearchShimmerNode: ASDisplayNode {
if let itemNode = itemNodes[sampleIndex] as? ChatListItemNode {
if !isInlineMode {
if !itemNode.avatarNode.isHidden {
context.fillEllipse(in: itemNode.avatarNode.frame.offsetBy(dx: 0.0, dy: currentY))
context.fillEllipse(in: itemNode.avatarNode.view.convert(itemNode.avatarNode.bounds, to: itemNode.view).offsetBy(dx: 0.0, dy: currentY))
}
}

View File

@ -1142,7 +1142,12 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
}
strongSelf.contextContainer.additionalActivationProgressLayer = nil
if item.interaction.inlineNavigationLocation != nil {
if let inlineNavigationLocation = item.interaction.inlineNavigationLocation {
if case let .peer(peerId) = inlineNavigationLocation.location {
if case let .chatList(index) = item.index, index.messageIndex.id.peerId == peerId {
return false
}
}
strongSelf.contextContainer.targetNodeForActivationProgress = strongSelf.avatarContainerNode
} else if let value = strongSelf.hitTest(location, with: nil), value === strongSelf.compoundTextButtonNode?.view {
strongSelf.contextContainer.targetNodeForActivationProgress = strongSelf.compoundTextButtonNode
@ -1313,9 +1318,6 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
reallyHighlighted = true
}
}
if item.interaction.inlineNavigationLocation != nil {
reallyHighlighted = false
}
}
return reallyHighlighted
}
@ -1403,7 +1405,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
let textFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
let dateFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0))
let badgeFont = Font.with(size: floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0), design: .regular, weight: .regular, traits: [.monospacedNumbers])
let avatarBadgeFont = Font.with(size: 16.0, design: .regular, weight: .regular, traits: [.monospacedNumbers])
let avatarBadgeFont = Font.with(size: floor(item.presentationData.fontSize.itemListBaseFontSize * 16.0 / 17.0), design: .regular, weight: .regular, traits: [.monospacedNumbers])
let account = item.context.account
var messages: [EngineMessage]
@ -1579,7 +1581,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
}
let badgeDiameter = floor(item.presentationData.fontSize.baseDisplaySize * 20.0 / 17.0)
let avatarBadgeDiameter: CGFloat = 22.0
let avatarBadgeDiameter: CGFloat = floor(floor(item.presentationData.fontSize.itemListBaseFontSize * 22.0 / 17.0))
let currentAvatarBadgeCleanBackgroundImage: UIImage? = PresentationResourcesChatList.badgeBackgroundBorder(item.presentationData.theme, diameter: avatarBadgeDiameter + 4.0)
@ -2354,7 +2356,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
itemHeight += titleSpacing
itemHeight += authorSpacing
let rawContentRect = CGRect(origin: CGPoint(x: 2.0, y: layoutOffset + 8.0), size: CGSize(width: rawContentWidth, height: itemHeight - 12.0 - 9.0))
let rawContentRect = CGRect(origin: CGPoint(x: 2.0, y: layoutOffset + floor(item.presentationData.fontSize.itemListBaseFontSize * 8.0 / 17.0)), size: CGSize(width: rawContentWidth, height: itemHeight - 12.0 - 9.0))
let insets = ChatListItemNode.insets(first: first, last: last, firstWithHeader: firstWithHeader)
var heightOffset: CGFloat = 0.0
@ -2407,7 +2409,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
var mainContentAlpha: CGFloat = 1.0
if case .chatList = item.chatListLocation {
mainContentFrame = CGRect(origin: CGPoint(x: params.leftInset + 72.0, y: 0.0), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height))
mainContentFrame = CGRect(origin: CGPoint(x: leftInset - 2.0, y: 0.0), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height))
mainContentBoundsOffset = mainContentFrame.origin.x
if let inlineNavigationLocation = item.interaction.inlineNavigationLocation {
@ -2497,7 +2499,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
var avatarScaleOffset: CGFloat = 0.0
var avatarScale: CGFloat = 1.0
if let inlineNavigationLocation = item.interaction.inlineNavigationLocation {
let targetAvatarScale: CGFloat = 54.0 / avatarFrame.width
let targetAvatarScale: CGFloat = floor(item.presentationData.fontSize.itemListBaseFontSize * 54.0 / 17.0) / avatarFrame.width
avatarScale = targetAvatarScale * inlineNavigationLocation.progress + 1.0 * (1.0 - inlineNavigationLocation.progress)
let targetAvatarScaleOffset: CGFloat = -(avatarFrame.width - avatarFrame.width * avatarScale) * 0.5
@ -2863,7 +2865,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
finalBottomRect
], color: theme.pinnedItemBackgroundColor.mixedWith(theme.unreadBadgeInactiveBackgroundColor, alpha: 0.1))
compoundTextButtonNode.frame = compoundHighlightingNode.frame
transition.updateFrame(node: compoundTextButtonNode, frame: compoundHighlightingNode.frame)
if let textArrowImage = textArrowImage {
let textArrowNode: ASImageNode
@ -3043,9 +3045,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
let titlePosition = strongSelf.titleNode.position
transition.animatePosition(node: strongSelf.titleNode, from: CGPoint(x: titlePosition.x - contentDelta.x, y: titlePosition.y - contentDelta.y))
transition.animatePositionAdditive(node: strongSelf.textNode.textNode, offset: CGPoint(x: -contentDelta.x, y: -contentDelta.y))
if let dustNode = strongSelf.dustNode {
transition.animatePositionAdditive(node: dustNode, offset: CGPoint(x: -contentDelta.x, y: -contentDelta.y))
if strongSelf.textNode.textNode.supernode === strongSelf.mainContentContainerNode {
transition.animatePositionAdditive(node: strongSelf.textNode.textNode, offset: CGPoint(x: -contentDelta.x, y: -contentDelta.y))
if let dustNode = strongSelf.dustNode {
transition.animatePositionAdditive(node: dustNode, offset: CGPoint(x: -contentDelta.x, y: -contentDelta.y))
}
}
let authorPosition = strongSelf.authorNode.position

View File

@ -20,7 +20,7 @@ import Postbox
public enum ChatListNodeMode {
case chatList
case peers(filter: ChatListNodePeersFilter, isSelecting: Bool, additionalCategories: [ChatListNodeAdditionalCategory], chatListFilters: [ChatListFilter]?)
case peers(filter: ChatListNodePeersFilter, isSelecting: Bool, additionalCategories: [ChatListNodeAdditionalCategory], chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool)
}
struct ChatListNodeListViewTransition {
@ -292,7 +292,7 @@ public struct ChatListNodeState: Equatable {
}
}
private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, filterData: ChatListItemFilterData?, mode: ChatListNodeMode, entries: [ChatListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] {
private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, filterData: ChatListItemFilterData?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, entries: [ChatListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] {
return entries.map { entry -> ListViewInsertItem in
switch entry.entry {
case .HeaderEntry:
@ -315,7 +315,28 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
nodeInteraction.additionalCategorySelected(id)
}
), directionHint: entry.directionHint)
case let .PeerEntry(index, presentationData, messages, combinedReadState, isRemovedFromTotalUnreadCount, draftState, peer, threadInfo, presence, hasUnseenMentions, hasUnseenReactions, editing, hasActiveRevealControls, selected, inputActivities, promoInfo, hasFailedMessages, isContact, forumTopicData, topForumTopicItems, revealed):
case let .PeerEntry(peerEntry):
let index = peerEntry.index
let presentationData = peerEntry.presentationData
let combinedReadState = peerEntry.readState
let isRemovedFromTotalUnreadCount = peerEntry.isRemovedFromTotalUnreadCount
let draftState = peerEntry.draftState
let peer = peerEntry.peer
let threadInfo = peerEntry.threadInfo
let presence = peerEntry.presence
let hasUnseenMentions = peerEntry.hasUnseenMentions
let hasUnseenReactions = peerEntry.hasUnseenReactions
let editing = peerEntry.editing
let hasActiveRevealControls = peerEntry.hasActiveRevealControls
let selected = peerEntry.selected
let inputActivities = peerEntry.inputActivities
let promoInfo = peerEntry.promoInfo
let hasFailedMessages = peerEntry.hasFailedMessages
let isContact = peerEntry.isContact
let forumTopicData = peerEntry.forumTopicData
let topForumTopicItems = peerEntry.topForumTopicItems
let revealed = peerEntry.revealed
switch mode {
case .chatList:
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(
@ -325,7 +346,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
filterData: filterData,
index: index,
content: .peer(
messages: messages,
messages: peerEntry.messages,
peer: peer,
threadInfo: threadInfo,
combinedReadState: combinedReadState,
@ -350,84 +371,90 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
hiddenOffset: threadInfo?.isHidden == true && !revealed,
interaction: nodeInteraction
), directionHint: entry.directionHint)
case let .peers(filter, isSelecting, _, filters):
case let .peers(filter, isSelecting, _, filters, displayAutoremoveTimeout):
let itemPeer = peer.chatMainPeer
var chatPeer: EnginePeer?
if let peer = peer.peers[peer.peerId] {
chatPeer = peer
}
var enabled = true
if filter.contains(.onlyWriteable) {
if let peer = peer.peers[peer.peerId] {
if !canSendMessagesToPeer(peer._asPeer()) {
if let isPeerEnabled {
if let itemPeer {
enabled = isPeerEnabled(itemPeer)
}
} else {
if filter.contains(.onlyWriteable) {
if let peer = peer.peers[peer.peerId] {
if !canSendMessagesToPeer(peer._asPeer()) {
enabled = false
}
} else {
enabled = false
}
} else {
enabled = false
if let threadInfo, threadInfo.isClosed, case let .channel(channel) = itemPeer {
if threadInfo.isOwnedByMe || channel.hasPermission(.manageTopics) {
} else {
enabled = false
}
}
}
if let threadInfo, threadInfo.isClosed, case let .channel(channel) = itemPeer {
if threadInfo.isOwnedByMe || channel.hasPermission(.manageTopics) {
if filter.contains(.onlyPrivateChats) {
if let peer = peer.peers[peer.peerId] {
switch peer {
case .user, .secretChat:
break
default:
enabled = false
}
} else {
enabled = false
}
}
}
if filter.contains(.onlyPrivateChats) {
if let peer = peer.peers[peer.peerId] {
switch peer {
case .user, .secretChat:
break
default:
enabled = false
}
} else {
enabled = false
}
}
if filter.contains(.onlyGroups) {
if let peer = peer.peers[peer.peerId] {
if case .legacyGroup = peer {
} else if case let .channel(peer) = peer, case .group = peer.info {
if filter.contains(.onlyGroups) {
if let peer = peer.peers[peer.peerId] {
if case .legacyGroup = peer {
} else if case let .channel(peer) = peer, case .group = peer.info {
} else {
enabled = false
}
} else {
enabled = false
}
} else {
enabled = false
}
}
if filter.contains(.onlyManageable) {
if let peer = peer.peers[peer.peerId] {
var canManage = false
if case let .legacyGroup(peer) = peer {
switch peer.role {
if filter.contains(.onlyManageable) {
if let peer = peer.peers[peer.peerId] {
var canManage = false
if case let .legacyGroup(peer) = peer {
switch peer.role {
case .creator, .admin:
canManage = true
default:
break
}
}
if canManage {
} else if case let .channel(peer) = peer, case .group = peer.info, peer.hasPermission(.inviteMembers) {
} else {
enabled = false
}
}
if canManage {
} else if case let .channel(peer) = peer, case .group = peer.info, peer.hasPermission(.inviteMembers) {
} else {
enabled = false
}
} else {
enabled = false
}
}
if filter.contains(.excludeChannels) {
if let peer = peer.peers[peer.peerId] {
if case let .channel(peer) = peer, case .broadcast = peer.info {
enabled = false
if filter.contains(.excludeChannels) {
if let peer = peer.peers[peer.peerId] {
if case let .channel(peer) = peer, case .broadcast = peer.info {
enabled = false
}
}
}
}
var header: ChatListSearchItemHeader?
switch mode {
case let .peers(_, _, additionalCategories, _):
case let .peers(_, _, additionalCategories, _, _):
if !additionalCategories.isEmpty {
let headerType: ChatListSearchItemHeaderType
if case .action = additionalCategories[0].appearance {
@ -444,8 +471,8 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
var status: ContactsPeerItemStatus = .none
if isSelecting, let itemPeer = itemPeer {
if let (string, multiline) = statusStringForPeerType(accountPeerId: context.account.peerId, strings: presentationData.strings, peer: itemPeer, isMuted: isRemovedFromTotalUnreadCount, isUnread: combinedReadState?.isUnread ?? false, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: filters) {
status = .custom(string: string, multiline: multiline)
if let (string, multiline, isActive, icon) = statusStringForPeerType(accountPeerId: context.account.peerId, strings: presentationData.strings, peer: itemPeer, isMuted: isRemovedFromTotalUnreadCount, isUnread: combinedReadState?.isUnread ?? false, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: filters, displayAutoremoveTimeout: displayAutoremoveTimeout, autoremoveTimeout: peerEntry.autoremoveTimeout) {
status = .custom(string: string, multiline: multiline, isActive: isActive, icon: icon)
} else {
status = .none
}
@ -541,10 +568,31 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
}
}
private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, filterData: ChatListItemFilterData?, mode: ChatListNodeMode, entries: [ChatListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] {
private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, filterData: ChatListItemFilterData?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, entries: [ChatListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] {
return entries.map { entry -> ListViewUpdateItem in
switch entry.entry {
case let .PeerEntry(index, presentationData, messages, combinedReadState, isRemovedFromTotalUnreadCount, draftState, peer, threadInfo, presence, hasUnseenMentions, hasUnseenReactions, editing, hasActiveRevealControls, selected, inputActivities, promoInfo, hasFailedMessages, isContact, forumTopicData, topForumTopicItems, revealed):
case let .PeerEntry(peerEntry):
let index = peerEntry.index
let presentationData = peerEntry.presentationData
let combinedReadState = peerEntry.readState
let isRemovedFromTotalUnreadCount = peerEntry.isRemovedFromTotalUnreadCount
let draftState = peerEntry.draftState
let peer = peerEntry.peer
let threadInfo = peerEntry.threadInfo
let presence = peerEntry.presence
let hasUnseenMentions = peerEntry.hasUnseenMentions
let hasUnseenReactions = peerEntry.hasUnseenReactions
let editing = peerEntry.editing
let hasActiveRevealControls = peerEntry.hasActiveRevealControls
let selected = peerEntry.selected
let inputActivities = peerEntry.inputActivities
let promoInfo = peerEntry.promoInfo
let hasFailedMessages = peerEntry.hasFailedMessages
let isContact = peerEntry.isContact
let forumTopicData = peerEntry.forumTopicData
let topForumTopicItems = peerEntry.topForumTopicItems
let revealed = peerEntry.revealed
switch mode {
case .chatList:
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(
@ -554,7 +602,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
filterData: filterData,
index: index,
content: .peer(
messages: messages,
messages: peerEntry.messages,
peer: peer,
threadInfo: threadInfo,
combinedReadState: combinedReadState,
@ -579,37 +627,44 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
hiddenOffset: threadInfo?.isHidden == true && !revealed,
interaction: nodeInteraction
), directionHint: entry.directionHint)
case let .peers(filter, isSelecting, _, filters):
case let .peers(filter, isSelecting, _, filters, displayAutoremoveTimeout):
let itemPeer = peer.chatMainPeer
var chatPeer: EnginePeer?
if let peer = peer.peers[peer.peerId] {
chatPeer = peer
}
var enabled = true
if filter.contains(.onlyWriteable) {
if let peer = peer.peers[peer.peerId] {
if !canSendMessagesToPeer(peer._asPeer()) {
enabled = false
}
} else {
enabled = false
if let isPeerEnabled {
if let itemPeer {
enabled = isPeerEnabled(itemPeer)
}
if let threadInfo, threadInfo.isClosed, case let .channel(channel) = itemPeer {
if threadInfo.isOwnedByMe || channel.hasPermission(.manageTopics) {
} else {
if filter.contains(.onlyWriteable) {
if let peer = peer.peers[peer.peerId] {
if !canSendMessagesToPeer(peer._asPeer()) {
enabled = false
}
} else {
enabled = false
}
if let threadInfo, threadInfo.isClosed, case let .channel(channel) = itemPeer {
if threadInfo.isOwnedByMe || channel.hasPermission(.manageTopics) {
} else {
enabled = false
}
}
}
if filter.contains(.excludeChannels) {
if case let .channel(peer) = peer.chatMainPeer, case .broadcast = peer.info {
enabled = false
}
}
}
if filter.contains(.excludeChannels) {
if case let .channel(peer) = peer.chatMainPeer, case .broadcast = peer.info {
enabled = false
}
}
var header: ChatListSearchItemHeader?
switch mode {
case let .peers(_, _, additionalCategories, _):
case let .peers(_, _, additionalCategories, _, _):
if !additionalCategories.isEmpty {
let headerType: ChatListSearchItemHeaderType
if case .action = additionalCategories[0].appearance {
@ -626,8 +681,8 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
var status: ContactsPeerItemStatus = .none
if isSelecting, let itemPeer = itemPeer {
if let (string, multiline) = statusStringForPeerType(accountPeerId: context.account.peerId, strings: presentationData.strings, peer: itemPeer, isMuted: isRemovedFromTotalUnreadCount, isUnread: combinedReadState?.isUnread ?? false, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: filters) {
status = .custom(string: string, multiline: multiline)
if let (string, multiline, isActive, icon) = statusStringForPeerType(accountPeerId: context.account.peerId, strings: presentationData.strings, peer: itemPeer, isMuted: isRemovedFromTotalUnreadCount, isUnread: combinedReadState?.isUnread ?? false, isContact: isContact, hasUnseenMentions: hasUnseenMentions, chatListFilters: filters, displayAutoremoveTimeout: displayAutoremoveTimeout, autoremoveTimeout: peerEntry.autoremoveTimeout) {
status = .custom(string: string, multiline: multiline, isActive: isActive, icon: icon)
} else {
status = .none
}
@ -743,8 +798,8 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
}
}
private func mappedChatListNodeViewListTransition(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, filterData: ChatListItemFilterData?, mode: ChatListNodeMode, transition: ChatListNodeViewTransition) -> ChatListNodeListViewTransition {
return ChatListNodeListViewTransition(chatListView: transition.chatListView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, nodeInteraction: nodeInteraction, location: location, filterData: filterData, mode: mode, entries: transition.insertEntries), updateItems: mappedUpdateEntries(context: context, nodeInteraction: nodeInteraction, location: location, filterData: filterData, mode: mode, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, adjustScrollToFirstItem: transition.adjustScrollToFirstItem, animateCrossfade: transition.animateCrossfade)
private func mappedChatListNodeViewListTransition(context: AccountContext, nodeInteraction: ChatListNodeInteraction, location: ChatListControllerLocation, filterData: ChatListItemFilterData?, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)?, transition: ChatListNodeViewTransition) -> ChatListNodeListViewTransition {
return ChatListNodeListViewTransition(chatListView: transition.chatListView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, nodeInteraction: nodeInteraction, location: location, filterData: filterData, mode: mode, isPeerEnabled: isPeerEnabled, entries: transition.insertEntries), updateItems: mappedUpdateEntries(context: context, nodeInteraction: nodeInteraction, location: location, filterData: filterData, mode: mode, isPeerEnabled: isPeerEnabled, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, adjustScrollToFirstItem: transition.adjustScrollToFirstItem, animateCrossfade: transition.animateCrossfade)
}
private final class ChatListOpaqueTransactionState {
@ -907,7 +962,7 @@ public final class ChatListNode: ListView {
let hideArhiveIntro = ValuePromise<Bool>(false, ignoreRepeated: true)
public init(context: AccountContext, location: ChatListControllerLocation, chatListFilter: ChatListFilter? = nil, previewing: Bool, fillPreloadItems: Bool, mode: ChatListNodeMode, theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, disableAnimations: Bool, isInlineMode: Bool) {
public init(context: AccountContext, location: ChatListControllerLocation, chatListFilter: ChatListFilter? = nil, previewing: Bool, fillPreloadItems: Bool, mode: ChatListNodeMode, isPeerEnabled: ((EnginePeer) -> Bool)? = nil, theme: PresentationTheme, fontSize: PresentationFontSize, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameSortOrder: PresentationPersonNameOrder, nameDisplayOrder: PresentationPersonNameOrder, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, disableAnimations: Bool, isInlineMode: Bool) {
self.context = context
self.location = location
self.chatListFilter = chatListFilter
@ -918,7 +973,7 @@ public final class ChatListNode: ListView {
self.animationRenderer = animationRenderer
var isSelecting = false
if case .peers(_, true, _, _) = mode {
if case .peers(_, true, _, _, _) = mode {
isSelecting = true
}
@ -950,6 +1005,12 @@ public final class ChatListNode: ListView {
guard let strongSelf = self else {
return
}
if case .peers = strongSelf.mode {
if let strongSelf = self, let peerSelected = strongSelf.peerSelected {
peerSelected(peer, nil, true, true, nil)
}
return
}
var didBeginSelecting = false
var count = 0
strongSelf.updateState { [weak self] state in
@ -1213,7 +1274,7 @@ public final class ChatListNode: ListView {
let currentRemovingItemId = self.currentRemovingItemId
let savedMessagesPeer: Signal<EnginePeer?, NoError>
if case let .peers(filter, _, _, _) = mode, filter.contains(.onlyWriteable), case .chatList = location {
if case let .peers(filter, _, _, _, _) = mode, filter.contains(.onlyWriteable), case .chatList = location {
savedMessagesPeer = context.account.postbox.loadedPeerWithId(context.account.peerId)
|> map(Optional.init)
|> map { peer in
@ -1269,11 +1330,13 @@ public final class ChatListNode: ListView {
let (rawEntries, isLoading) = chatListNodeEntriesForView(update.list, state: state, savedMessagesPeer: savedMessagesPeer, foundPeers: state.foundPeers, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, mode: mode, chatListLocation: location)
let entries = rawEntries.filter { entry in
switch entry {
case let .PeerEntry(_, _, _, _, _, _, peer, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
case let .PeerEntry(peerEntry):
let peer = peerEntry.peer
switch mode {
case .chatList:
return true
case let .peers(filter, _, _, _):
case let .peers(filter, _, _, _, _):
guard !filter.contains(.excludeSavedMessages) || peer.peerId != currentPeerId else { return false }
guard !filter.contains(.excludeSavedMessages) || !peer.peerId.isReplies else { return false }
guard !filter.contains(.excludeSecretChats) || peer.peerId.namespace != Namespaces.Peer.SecretChat else { return false }
@ -1444,7 +1507,10 @@ public final class ChatListNode: ListView {
var didIncludeHiddenThread = false
if let previous = previousView {
for entry in previous.filteredEntries {
if case let .PeerEntry(index, _, _, _, _, _, _, threadInfo, _, _, _, _, _, _, _, _, _, _, _, _, _) = entry {
if case let .PeerEntry(peerEntry) = entry {
let index = peerEntry.index
let threadInfo = peerEntry.threadInfo
if let threadInfo, threadInfo.isHidden {
didIncludeHiddenThread = true
}
@ -1474,7 +1540,10 @@ public final class ChatListNode: ListView {
var doesIncludeHiddenThread = false
for entry in processedView.filteredEntries {
if case let .PeerEntry(index, _, _, _, _, _, _, threadInfo, _, _, _, _, _, _, _, _, _, _, _, _, _) = entry {
if case let .PeerEntry(peerEntry) = entry {
let index = peerEntry.index
let threadInfo = peerEntry.threadInfo
if let threadInfo, threadInfo.isHidden {
doesIncludeHiddenThread = true
}
@ -1547,7 +1616,7 @@ public final class ChatListNode: ListView {
}
return preparedChatListNodeViewTransition(from: previousView, to: processedView, reason: reason, previewing: previewing, disableAnimations: disableAnimations, account: context.account, scrollPosition: updatedScrollPosition, searchMode: searchMode)
|> map({ mappedChatListNodeViewListTransition(context: context, nodeInteraction: nodeInteraction, location: location, filterData: filterData, mode: mode, transition: $0) })
|> map({ mappedChatListNodeViewListTransition(context: context, nodeInteraction: nodeInteraction, location: location, filterData: filterData, mode: mode, isPeerEnabled: isPeerEnabled, transition: $0) })
|> runOn(prepareOnMainQueue ? Queue.mainQueue() : viewProcessingQueue)
}
@ -1585,7 +1654,9 @@ public final class ChatListNode: ListView {
continue
}
switch chatListView.filteredEntries[entryCount - i - 1] {
case let .PeerEntry(_, _, _, _, _, _, _, threadInfo, _, _, _, _, _, _, _, _, _, _, _, _, _):
case let .PeerEntry(peerEntry):
let threadInfo = peerEntry.threadInfo
if let threadInfo, threadInfo.isHidden {
isHiddenItemVisible = true
}
@ -1792,7 +1863,10 @@ public final class ChatListNode: ListView {
var referenceId: EngineChatList.PinnedItem.Id?
var beforeAll = false
switch toEntry {
case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _, promoInfo, _, _, _, _, _):
case let .PeerEntry(peerEntry):
let index = peerEntry.index
let promoInfo = peerEntry.promoInfo
if promoInfo != nil {
beforeAll = true
} else {
@ -1819,8 +1893,8 @@ public final class ChatListNode: ListView {
var itemId: EngineChatList.PinnedItem.Id?
switch fromEntry {
case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
if case let .chatList(index) = index {
case let .PeerEntry(peerEntry):
if case let .chatList(index) = peerEntry.index {
itemId = .peer(index.messageIndex.id.peerId)
}
default:
@ -1865,11 +1939,11 @@ public final class ChatListNode: ListView {
var referenceId: Int64?
var beforeAll = false
switch toEntry {
case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _, promoInfo, _, _, _, _, _):
if promoInfo != nil {
case let .PeerEntry(peerEntry):
if peerEntry.promoInfo != nil {
beforeAll = true
} else {
if case let .forum(_, _, threadId, _, _) = index {
if case let .forum(_, _, threadId, _, _) = peerEntry.index {
referenceId = threadId
}
}
@ -1885,8 +1959,8 @@ public final class ChatListNode: ListView {
var itemId: Int64?
switch fromEntry {
case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
if case let .forum(_, _, threadId, _, _) = index {
case let .PeerEntry(peerEntry):
if case let .forum(_, _, threadId, _, _) = peerEntry.index {
itemId = threadId
}
default:
@ -2154,15 +2228,15 @@ public final class ChatListNode: ListView {
if !transition.chatListView.originalList.hasLater {
for entry in filteredEntries.reversed() {
switch entry {
case let .PeerEntry(index, _, _, combinedReadState, isMuted, _, _, _, _, _, _, _, _, _, _, promoInfo, _, _, _, _, _):
if promoInfo == nil {
case let .PeerEntry(peerEntry):
if peerEntry.promoInfo == nil {
var hasUnread = false
if let combinedReadState = combinedReadState {
if let combinedReadState = peerEntry.readState {
hasUnread = combinedReadState.count > 0
}
switch index {
switch peerEntry.index {
case let .chatList(index):
preloadItems.append(ChatHistoryPreloadItem(index: index, threadId: nil, isMuted: isMuted, hasUnread: hasUnread))
preloadItems.append(ChatHistoryPreloadItem(index: index, threadId: nil, isMuted: peerEntry.isRemovedFromTotalUnreadCount, hasUnread: hasUnread))
case .forum:
break
}
@ -2271,7 +2345,7 @@ public final class ChatListNode: ListView {
case .GroupReferenceEntry, .HoleEntry, .PeerEntry:
containsChats = true
if case .forum = strongSelf.location {
if case let .PeerEntry(_, _, _, _, _, _, _, threadInfo, _, _, _, _, _, _, _, _, _, _, _, _, _) = entry, let threadInfo {
if case let .PeerEntry(peerEntry) = entry, let threadInfo = peerEntry.threadInfo {
if threadInfo.id == 1 {
hasGeneral = true
}
@ -2527,9 +2601,9 @@ public final class ChatListNode: ListView {
continue
}
switch chatListView.filteredEntries[entryCount - i - 1] {
case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
if interaction.highlightedChatLocation?.location == ChatLocation.peer(id: peer.peerId) {
current = (index, peer.peer!, entryCount - i - 1)
case let .PeerEntry(peerEntry):
if interaction.highlightedChatLocation?.location == ChatLocation.peer(id: peerEntry.peer.peerId) {
current = (peerEntry.index, peerEntry.peer.peer!, entryCount - i - 1)
break outer
}
default:
@ -2574,11 +2648,11 @@ public final class ChatListNode: ListView {
case .previous(unread: false), .next(unread: false):
var target: (EngineChatList.Item.Index, EnginePeer)? = nil
if let current = current, entryCount > 1 {
if current.2 > 0, case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = chatListView.filteredEntries[current.2 - 1] {
next = (index, peer.peer!)
if current.2 > 0, case let .PeerEntry(peerEntry) = chatListView.filteredEntries[current.2 - 1] {
next = (peerEntry.index, peerEntry.peer.peer!)
}
if current.2 <= entryCount - 2, case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = chatListView.filteredEntries[current.2 + 1] {
previous = (index, peer.peer!)
if current.2 <= entryCount - 2, case let .PeerEntry(peerEntry) = chatListView.filteredEntries[current.2 + 1] {
previous = (peerEntry.index, peerEntry.peer.peer!)
}
if case .previous = option {
target = previous
@ -2586,8 +2660,8 @@ public final class ChatListNode: ListView {
target = next
}
} else if entryCount > 0 {
if case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = chatListView.filteredEntries[entryCount - 1] {
target = (index, peer.peer!)
if case let .PeerEntry(peerEntry) = chatListView.filteredEntries[entryCount - 1] {
target = (peerEntry.index, peerEntry.peer.peer!)
}
}
if let target = target {
@ -2664,8 +2738,8 @@ public final class ChatListNode: ListView {
continue
}
switch chatListView.filteredEntries[entryCount - i - 1] {
case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
return index
case let .PeerEntry(peerEntry):
return peerEntry.index
default:
break
}
@ -2889,11 +2963,21 @@ public final class ChatListNode: ListView {
}
}
private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: PresentationStrings, peer: EnginePeer, isMuted: Bool, isUnread: Bool, isContact: Bool, hasUnseenMentions: Bool, chatListFilters: [ChatListFilter]?) -> (String, Bool)? {
private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: PresentationStrings, peer: EnginePeer, isMuted: Bool, isUnread: Bool, isContact: Bool, hasUnseenMentions: Bool, chatListFilters: [ChatListFilter]?, displayAutoremoveTimeout: Bool, autoremoveTimeout: Int32?) -> (String, Bool, Bool, ContactsPeerItemStatus.Icon?)? {
if accountPeerId == peer.id {
return nil
}
if displayAutoremoveTimeout {
if let autoremoveTimeout = autoremoveTimeout {
//TODO:localize
return ("auto-delete after \(timeIntervalString(strings: strings, value: autoremoveTimeout))", false, true, .autoremove)
} else {
//TODO:localize
return ("auto-deletion disabled", false, false, .autoremove)
}
}
if let chatListFilters = chatListFilters {
var result = ""
for case let .filter(_, title, _, data) in chatListFilters {
@ -2909,7 +2993,7 @@ private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: Pres
if result.isEmpty {
return nil
} else {
return (result, true)
return (result, true, false, nil)
}
}
@ -2917,28 +3001,28 @@ private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: Pres
return nil
} else if case let .user(user) = peer {
if user.botInfo != nil || user.flags.contains(.isSupport) {
return (strings.ChatList_PeerTypeBot, false)
return (strings.ChatList_PeerTypeBot, false, false, nil)
} else if isContact {
return (strings.ChatList_PeerTypeContact, false)
return (strings.ChatList_PeerTypeContact, false, false, nil)
} else {
return (strings.ChatList_PeerTypeNonContact, false)
return (strings.ChatList_PeerTypeNonContact, false, false, nil)
}
} else if case .secretChat = peer {
if isContact {
return (strings.ChatList_PeerTypeContact, false)
return (strings.ChatList_PeerTypeContact, false, false, nil)
} else {
return (strings.ChatList_PeerTypeNonContact, false)
return (strings.ChatList_PeerTypeNonContact, false, false, nil)
}
} else if case .legacyGroup = peer {
return (strings.ChatList_PeerTypeGroup, false)
return (strings.ChatList_PeerTypeGroup, false, false, nil)
} else if case let .channel(channel) = peer {
if case .group = channel.info {
return (strings.ChatList_PeerTypeGroup, false)
return (strings.ChatList_PeerTypeGroup, false, false, nil)
} else {
return (strings.ChatList_PeerTypeChannel, false)
return (strings.ChatList_PeerTypeChannel, false, false, nil)
}
}
return (strings.ChatList_PeerTypeNonContact, false)
return (strings.ChatList_PeerTypeNonContact, false, false, nil)
}
public class ChatHistoryListSelectionRecognizer: UIPanGestureRecognizer {

View File

@ -46,30 +46,191 @@ public enum ChatListNodeEntryPromoInfo: Equatable {
}
enum ChatListNodeEntry: Comparable, Identifiable {
struct PeerEntryData: Equatable {
var index: EngineChatList.Item.Index
var presentationData: ChatListPresentationData
var messages: [EngineMessage]
var readState: EnginePeerReadCounters?
var isRemovedFromTotalUnreadCount: Bool
var draftState: ChatListItemContent.DraftState?
var peer: EngineRenderedPeer
var threadInfo: ChatListItemContent.ThreadInfo?
var presence: EnginePeer.Presence?
var hasUnseenMentions: Bool
var hasUnseenReactions: Bool
var editing: Bool
var hasActiveRevealControls: Bool
var selected: Bool
var inputActivities: [(EnginePeer, PeerInputActivity)]?
var promoInfo: ChatListNodeEntryPromoInfo?
var hasFailedMessages: Bool
var isContact: Bool
var autoremoveTimeout: Int32?
var forumTopicData: EngineChatList.ForumTopicData?
var topForumTopicItems: [EngineChatList.ForumTopicData]
var revealed: Bool
init(
index: EngineChatList.Item.Index,
presentationData: ChatListPresentationData,
messages: [EngineMessage],
readState: EnginePeerReadCounters?,
isRemovedFromTotalUnreadCount: Bool,
draftState: ChatListItemContent.DraftState?,
peer: EngineRenderedPeer,
threadInfo: ChatListItemContent.ThreadInfo?,
presence: EnginePeer.Presence?,
hasUnseenMentions: Bool,
hasUnseenReactions: Bool,
editing: Bool,
hasActiveRevealControls: Bool,
selected: Bool,
inputActivities: [(EnginePeer, PeerInputActivity)]?,
promoInfo: ChatListNodeEntryPromoInfo?,
hasFailedMessages: Bool,
isContact: Bool,
autoremoveTimeout: Int32?,
forumTopicData: EngineChatList.ForumTopicData?,
topForumTopicItems: [EngineChatList.ForumTopicData],
revealed: Bool
) {
self.index = index
self.presentationData = presentationData
self.messages = messages
self.readState = readState
self.isRemovedFromTotalUnreadCount = isRemovedFromTotalUnreadCount
self.draftState = draftState
self.peer = peer
self.threadInfo = threadInfo
self.presence = presence
self.hasUnseenMentions = hasUnseenMentions
self.hasUnseenReactions = hasUnseenReactions
self.editing = editing
self.hasActiveRevealControls = hasActiveRevealControls
self.selected = selected
self.inputActivities = inputActivities
self.promoInfo = promoInfo
self.hasFailedMessages = hasFailedMessages
self.isContact = isContact
self.autoremoveTimeout = autoremoveTimeout
self.forumTopicData = forumTopicData
self.topForumTopicItems = topForumTopicItems
self.revealed = revealed
}
static func ==(lhs: PeerEntryData, rhs: PeerEntryData) -> Bool {
if lhs.index != rhs.index {
return false
}
if lhs.presentationData !== rhs.presentationData {
return false
}
if lhs.readState != rhs.readState {
return false
}
if lhs.messages.count != rhs.messages.count {
return false
}
for i in 0 ..< lhs.messages.count {
if lhs.messages[i].stableVersion != rhs.messages[i].stableVersion {
return false
}
if lhs.messages[i].id != rhs.messages[i].id {
return false
}
if lhs.messages[i].associatedMessages.count != rhs.messages[i].associatedMessages.count {
return false
}
for (id, message) in lhs.messages[i].associatedMessages {
if let otherMessage = rhs.messages[i].associatedMessages[id] {
if message.stableVersion != otherMessage.stableVersion {
return false
}
} else {
return false
}
}
}
if lhs.isRemovedFromTotalUnreadCount != rhs.isRemovedFromTotalUnreadCount {
return false
}
if let lhsPeerPresence = lhs.presence, let rhsPeerPresence = rhs.presence {
if lhsPeerPresence != rhsPeerPresence {
return false
}
} else if (lhs.presence != nil) != (rhs.presence != nil) {
return false
}
if let lhsEmbeddedState = lhs.draftState, let rhsEmbeddedState = rhs.draftState {
if lhsEmbeddedState != rhsEmbeddedState {
return false
}
} else if (lhs.draftState != nil) != (rhs.draftState != nil) {
return false
}
if lhs.editing != rhs.editing {
return false
}
if lhs.hasActiveRevealControls != rhs.hasActiveRevealControls {
return false
}
if lhs.selected != rhs.selected {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.threadInfo != rhs.threadInfo {
return false
}
if lhs.hasUnseenMentions != rhs.hasUnseenMentions {
return false
}
if lhs.hasUnseenReactions != rhs.hasUnseenReactions {
return false
}
if let lhsInputActivities = lhs.inputActivities, let rhsInputActivities = rhs.inputActivities {
if lhsInputActivities.count != rhsInputActivities.count {
return false
}
for i in 0 ..< lhsInputActivities.count {
if lhsInputActivities[i].0 != rhsInputActivities[i].0 {
return false
}
if lhsInputActivities[i].1 != rhsInputActivities[i].1 {
return false
}
}
} else if (lhs.inputActivities != nil) != (rhs.inputActivities != nil) {
return false
}
if lhs.promoInfo != rhs.promoInfo {
return false
}
if lhs.hasFailedMessages != rhs.hasFailedMessages {
return false
}
if lhs.isContact != rhs.isContact {
return false
}
if lhs.autoremoveTimeout != rhs.autoremoveTimeout {
return false
}
if lhs.forumTopicData != rhs.forumTopicData {
return false
}
if lhs.topForumTopicItems != rhs.topForumTopicItems {
return false
}
if lhs.revealed != rhs.revealed {
return false
}
return true
}
}
case HeaderEntry
case PeerEntry(
index: EngineChatList.Item.Index,
presentationData: ChatListPresentationData,
messages: [EngineMessage],
readState: EnginePeerReadCounters?,
isRemovedFromTotalUnreadCount: Bool,
draftState: ChatListItemContent.DraftState?,
peer: EngineRenderedPeer,
threadInfo: ChatListItemContent.ThreadInfo?,
presence: EnginePeer.Presence?,
hasUnseenMentions: Bool,
hasUnseenReactions: Bool,
editing: Bool,
hasActiveRevealControls: Bool,
selected: Bool,
inputActivities: [(EnginePeer, PeerInputActivity)]?,
promoInfo: ChatListNodeEntryPromoInfo?,
hasFailedMessages: Bool,
isContact: Bool,
forumTopicData: EngineChatList.ForumTopicData?,
topForumTopicItems: [EngineChatList.ForumTopicData],
revealed: Bool
)
case PeerEntry(PeerEntryData)
case HoleEntry(EngineMessage.Index, theme: PresentationTheme)
case GroupReferenceEntry(index: EngineChatList.Item.Index, presentationData: ChatListPresentationData, groupId: EngineChatList.Group, peers: [EngineChatList.GroupItem.Item], message: EngineMessage?, editing: Bool, unreadCount: Int, revealed: Bool, hiddenByDefault: Bool)
case ArchiveIntro(presentationData: ChatListPresentationData)
@ -79,8 +240,8 @@ enum ChatListNodeEntry: Comparable, Identifiable {
switch self {
case .HeaderEntry:
return .index(.chatList(.absoluteUpperBound))
case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
return .index(index)
case let .PeerEntry(peerEntry):
return .index(peerEntry.index)
case let .HoleEntry(holeIndex, _):
return .index(.chatList(EngineChatList.Item.Index.ChatList(pinningIndex: nil, messageIndex: holeIndex)))
case let .GroupReferenceEntry(index, _, _, _, _, _, _, _, _):
@ -96,8 +257,8 @@ enum ChatListNodeEntry: Comparable, Identifiable {
switch self {
case .HeaderEntry:
return .Header
case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
switch index {
case let .PeerEntry(peerEntry):
switch peerEntry.index {
case let .chatList(index):
return .PeerId(index.messageIndex.id.peerId.toInt64())
case let .forum(_, _, threadId, _, _):
@ -126,115 +287,11 @@ enum ChatListNodeEntry: Comparable, Identifiable {
} else {
return false
}
case let .PeerEntry(lhsIndex, lhsPresentationData, lhsMessages, lhsUnreadCount, lhsIsRemovedFromTotalUnreadCount, lhsEmbeddedState, lhsPeer, lhsThreadInfo, lhsPresence, lhsHasUnseenMentions, lhsHasUnseenReactions, lhsEditing, lhsHasRevealControls, lhsSelected, lhsInputActivities, lhsAd, lhsHasFailedMessages, lhsIsContact, lhsForumThreadTitle, lhsTopForumTopicItems, lhsRevealed):
switch rhs {
case let .PeerEntry(rhsIndex, rhsPresentationData, rhsMessages, rhsUnreadCount, rhsIsRemovedFromTotalUnreadCount, rhsEmbeddedState, rhsPeer, rhsThreadInfo, rhsPresence, rhsHasUnseenMentions, rhsHasUnseenReactions, rhsEditing, rhsHasRevealControls, rhsSelected, rhsInputActivities, rhsAd, rhsHasFailedMessages, rhsIsContact, rhsForumThreadTitle, rhsTopForumTopicItems, rhsRevealed):
if lhsIndex != rhsIndex {
return false
}
if lhsPresentationData !== rhsPresentationData {
return false
}
if lhsUnreadCount != rhsUnreadCount {
return false
}
if lhsMessages.count != rhsMessages.count {
return false
}
for i in 0 ..< lhsMessages.count {
if lhsMessages[i].stableVersion != rhsMessages[i].stableVersion {
return false
}
if lhsMessages[i].id != rhsMessages[i].id {
return false
}
if lhsMessages[i].associatedMessages.count != rhsMessages[i].associatedMessages.count {
return false
}
for (id, message) in lhsMessages[i].associatedMessages {
if let otherMessage = rhsMessages[i].associatedMessages[id] {
if message.stableVersion != otherMessage.stableVersion {
return false
}
} else {
return false
}
}
}
if lhsIsRemovedFromTotalUnreadCount != rhsIsRemovedFromTotalUnreadCount {
return false
}
if let lhsPeerPresence = lhsPresence, let rhsPeerPresence = rhsPresence {
if lhsPeerPresence != rhsPeerPresence {
return false
}
} else if (lhsPresence != nil) != (rhsPresence != nil) {
return false
}
if let lhsEmbeddedState = lhsEmbeddedState, let rhsEmbeddedState = rhsEmbeddedState {
if lhsEmbeddedState != rhsEmbeddedState {
return false
}
} else if (lhsEmbeddedState != nil) != (rhsEmbeddedState != nil) {
return false
}
if lhsEditing != rhsEditing {
return false
}
if lhsHasRevealControls != rhsHasRevealControls {
return false
}
if lhsSelected != rhsSelected {
return false
}
if lhsPeer != rhsPeer {
return false
}
if lhsThreadInfo != rhsThreadInfo {
return false
}
if lhsHasUnseenMentions != rhsHasUnseenMentions {
return false
}
if lhsHasUnseenReactions != rhsHasUnseenReactions {
return false
}
if let lhsInputActivities = lhsInputActivities, let rhsInputActivities = rhsInputActivities {
if lhsInputActivities.count != rhsInputActivities.count {
return false
}
for i in 0 ..< lhsInputActivities.count {
if lhsInputActivities[i].0 != rhsInputActivities[i].0 {
return false
}
if lhsInputActivities[i].1 != rhsInputActivities[i].1 {
return false
}
}
} else if (lhsInputActivities != nil) != (rhsInputActivities != nil) {
return false
}
if lhsAd != rhsAd {
return false
}
if lhsHasFailedMessages != rhsHasFailedMessages {
return false
}
if lhsIsContact != rhsIsContact {
return false
}
if lhsForumThreadTitle != rhsForumThreadTitle {
return false
}
if lhsTopForumTopicItems != rhsTopForumTopicItems {
return false
}
if lhsRevealed != rhsRevealed {
return false
}
return true
default:
return false
case let .PeerEntry(peerEntry):
if case .PeerEntry(peerEntry) = rhs {
return true
} else {
return false
}
case let .HoleEntry(lhsHole, lhsTheme):
switch rhs {
@ -401,7 +458,7 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState
threadInfo = ChatListItemContent.ThreadInfo(id: threadId, info: threadData.info, isOwnedByMe: threadData.isOwnedByMe, isClosed: threadData.isClosed, isHidden: threadData.isHidden)
}
result.append(.PeerEntry(
result.append(.PeerEntry(ChatListNodeEntry.PeerEntryData(
index: offsetPinnedIndex(entry.index, offset: pinnedIndexOffset),
presentationData: state.presentationData,
messages: updatedMessages,
@ -420,10 +477,11 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState
promoInfo: nil,
hasFailedMessages: entry.hasFailed,
isContact: entry.isContact,
autoremoveTimeout: entry.autoremoveTimeout,
forumTopicData: entry.forumTopicData,
topForumTopicItems: entry.topForumTopicItems,
revealed: threadId == 1 && (state.hiddenItemShouldBeTemporaryRevealed || state.editing)
))
)))
}
if !view.hasLater {
var pinningIndex: UInt16 = UInt16(pinnedIndexOffset == 0 ? 0 : (pinnedIndexOffset - 1))
@ -438,7 +496,7 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState
}
let messageIndex = EngineMessage.Index(id: EngineMessage.Id(peerId: peer.0.id, namespace: 0, id: 0), timestamp: 1)
result.append(.PeerEntry(
result.append(.PeerEntry(ChatListNodeEntry.PeerEntryData(
index: .chatList(EngineChatList.Item.Index.ChatList(pinningIndex: foundPinningIndex, messageIndex: messageIndex)),
presentationData: state.presentationData,
messages: [],
@ -457,17 +515,18 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState
promoInfo: nil,
hasFailedMessages: false,
isContact: false,
autoremoveTimeout: nil,
forumTopicData: nil,
topForumTopicItems: [],
revealed: false
))
)))
if foundPinningIndex != 0 {
foundPinningIndex -= 1
}
}
}
result.append(.PeerEntry(
result.append(.PeerEntry(ChatListNodeEntry.PeerEntryData(
index: .chatList(EngineChatList.Item.Index.ChatList.absoluteUpperBound.predecessor),
presentationData: state.presentationData,
messages: [],
@ -486,10 +545,11 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState
promoInfo: nil,
hasFailedMessages: false,
isContact: false,
autoremoveTimeout: nil,
forumTopicData: nil,
topForumTopicItems: [],
revealed: false
))
)))
} else {
if !filteredAdditionalItemEntries.isEmpty {
for item in filteredAdditionalItemEntries.reversed() {
@ -516,8 +576,7 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState
default:
break
}
result.append(.PeerEntry(
result.append(.PeerEntry(ChatListNodeEntry.PeerEntryData(
index: .chatList(EngineChatList.Item.Index.ChatList(pinningIndex: pinningIndex, messageIndex: index.messageIndex)),
presentationData: state.presentationData,
messages: item.item.messages,
@ -536,10 +595,11 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState
promoInfo: promoInfo,
hasFailedMessages: item.item.hasFailed,
isContact: item.item.isContact,
autoremoveTimeout: item.item.autoremoveTimeout,
forumTopicData: item.item.forumTopicData,
topForumTopicItems: item.item.topForumTopicItems,
revealed: threadId == 1 && (state.hiddenItemShouldBeTemporaryRevealed || state.editing)
))
revealed: threadId == 1 && (state.hiddenItemShouldBeTemporaryRevealed || state.editing)
)))
if pinningIndex != 0 {
pinningIndex -= 1
}
@ -573,7 +633,7 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState
result.append(.HeaderEntry)
}
if !view.hasLater, case let .peers(_, _, additionalCategories, _) = mode {
if !view.hasLater, case let .peers(_, _, additionalCategories, _, _) = mode {
var index = 0
for category in additionalCategories.reversed(){
result.append(.AdditionalCategory(index: index, id: category.id, title: category.title, image: category.icon, appearance: category.appearance, selected: state.selectedAdditionalCategoryIds.contains(category.id), presentationData: state.presentationData))

View File

@ -287,7 +287,8 @@ func chatListViewForLocation(chatListLocation: ChatListControllerLocation, locat
forumTopicData: nil,
topForumTopicItems: [],
hasFailed: false,
isContact: false
isContact: false,
autoremoveTimeout: nil
))
}

View File

@ -184,19 +184,19 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
if let _ = peer as? TelegramUser {
status = .presence(presence ?? EnginePeer.Presence(status: .longTimeAgo, lastActivity: 0), dateTimeFormat)
} else if let group = peer as? TelegramGroup {
status = .custom(string: strings.Conversation_StatusMembers(Int32(group.participantCount)), multiline: false)
status = .custom(string: strings.Conversation_StatusMembers(Int32(group.participantCount)), multiline: false, isActive: false, icon: nil)
} else if let channel = peer as? TelegramChannel {
if case .group = channel.info {
if let participantCount = participantCount, participantCount != 0 {
status = .custom(string: strings.Conversation_StatusMembers(participantCount), multiline: false)
status = .custom(string: strings.Conversation_StatusMembers(participantCount), multiline: false, isActive: false, icon: nil)
} else {
status = .custom(string: strings.Group_Status, multiline: false)
status = .custom(string: strings.Group_Status, multiline: false, isActive: false, icon: nil)
}
} else {
if let participantCount = participantCount, participantCount != 0 {
status = .custom(string: strings.Conversation_StatusSubscribers(participantCount), multiline: false)
status = .custom(string: strings.Conversation_StatusSubscribers(participantCount), multiline: false, isActive: false, icon: nil)
} else {
status = .custom(string: strings.Channel_Status, multiline: false)
status = .custom(string: strings.Channel_Status, multiline: false, isActive: false, icon: nil)
}
}
} else {
@ -1267,7 +1267,7 @@ public final class ContactListNode: ASDisplayNode {
return context.engine.data.get(EngineDataMap(
view.entries.compactMap { entry -> EnginePeer.Id? in
switch entry {
case let .MessageEntry(_, _, _, _, _, renderedPeer, _, _, _, _, _, _):
case let .MessageEntry(_, _, _, _, _, renderedPeer, _, _, _, _, _, _, _):
if let peer = renderedPeer.peer {
if let channel = peer as? TelegramChannel, case .group = channel.info {
return peer.id
@ -1283,7 +1283,7 @@ public final class ContactListNode: ASDisplayNode {
var peers: [(EnginePeer, Int32)] = []
for entry in view.entries {
switch entry {
case let .MessageEntry(_, _, _, _, _, renderedPeer, _, _, _, _, _, _):
case let .MessageEntry(_, _, _, _, _, renderedPeer, _, _, _, _, _, _, _):
if let peer = renderedPeer.peer {
if peer is TelegramGroup {
peers.append((EnginePeer(peer), 0))

View File

@ -52,7 +52,7 @@ private enum InviteContactsEntry: Comparable, Identifiable {
case let .peer(_, id, contact, count, selection, theme, strings, nameSortOrder, nameDisplayOrder):
let status: ContactsPeerItemStatus
if count != 0 {
status = .custom(string: strings.Contacts_ImportersCount(count), multiline: false)
status = .custom(string: strings.Contacts_ImportersCount(count), multiline: false, isActive: false, icon: nil)
} else {
status = .none
}

View File

@ -31,10 +31,14 @@ public final class ContactItemHighlighting {
}
public enum ContactsPeerItemStatus {
public enum Icon {
case autoremove
}
case none
case presence(EnginePeer.Presence, PresentationDateTimeFormat)
case addressName(String)
case custom(string: String, multiline: Bool)
case custom(string: String, multiline: Bool, isActive: Bool, icon: Icon?)
}
public enum ContactsPeerItemSelection: Equatable {
@ -395,6 +399,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
private var credibilityIconView: ComponentHostView<Empty>?
private var credibilityIconComponent: EmojiStatusComponent?
private let statusNode: TextNode
private var statusIconNode: ASImageNode?
private var badgeBackgroundNode: ASImageNode?
private var badgeTextNode: TextNode?
private var selectionNode: CheckNode?
@ -733,6 +738,8 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
var titleAttributedString: NSAttributedString?
var statusAttributedString: NSAttributedString?
var statusIcon: ContactsPeerItemStatus.Icon?
var statusIsActive: Bool = false
var multilineStatus: Bool = false
var userPresence: EnginePeer.Presence?
@ -801,8 +808,10 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
} else if !suffix.isEmpty {
statusAttributedString = NSAttributedString(string: suffix, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
}
case let .custom(text, multiline):
statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
case let .custom(text, multiline, isActive, icon):
statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: isActive ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemSecondaryTextColor)
statusIcon = icon
statusIsActive = isActive
multilineStatus = multiline
}
}
@ -824,9 +833,11 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
}
switch item.status {
case let .custom(text, multiline):
statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
case let .custom(text, multiline, isActive, icon):
statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: isActive ? item.presentationData.theme.list.itemAccentColor : item.presentationData.theme.list.itemSecondaryTextColor)
multilineStatus = multiline
statusIsActive = isActive
statusIcon = icon
default:
break
}
@ -882,7 +893,20 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, params.width - leftInset - rightInset - additionalTitleInset), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 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()))
var maxStatusWidth: CGFloat = params.width - leftInset - rightInset - badgeSize
if let _ = statusIcon {
maxStatusWidth -= 10.0
}
let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: multilineStatus ? 3 : 1, truncationType: .end, constrainedSize: CGSize(width: max(0.0, maxStatusWidth), height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
var statusIconImage: UIImage?
if let statusIcon = statusIcon {
switch statusIcon {
case .autoremove:
statusIconImage = PresentationResourcesChatList.statusAutoremoveIcon(item.presentationData.theme, isActive: statusIsActive)
}
}
let titleVerticalInset: CGFloat = statusAttributedString == nil ? 13.0 : 6.0
let verticalInset: CGFloat = statusAttributedString == nil ? 13.0 : 6.0
@ -1097,12 +1121,32 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
strongSelf.statusNode.alpha = item.enabled ? 1.0 : 1.0
let _ = statusApply()
let statusFrame = CGRect(origin: CGPoint(x: revealOffset + leftInset, y: strongSelf.titleNode.frame.maxY - 1.0), size: statusLayout.size)
var statusFrame = CGRect(origin: CGPoint(x: revealOffset + leftInset, y: strongSelf.titleNode.frame.maxY - 1.0), size: statusLayout.size)
if let statusIconImage {
statusFrame.origin.x += statusIconImage.size.width + 1.0
}
let previousStatusFrame = strongSelf.statusNode.frame
strongSelf.statusNode.frame = statusFrame
transition.animatePositionAdditive(node: strongSelf.statusNode, offset: CGPoint(x: previousStatusFrame.minX - statusFrame.minX, y: 0))
if let statusIconImage {
let statusIconNode: ASImageNode
if let current = strongSelf.statusIconNode {
statusIconNode = current
} else {
statusIconNode = ASImageNode()
strongSelf.statusNode.addSubnode(statusIconNode)
}
statusIconNode.image = statusIconImage
statusIconNode.frame = CGRect(origin: CGPoint(x: -statusIconImage.size.width - 1.0, y: floor((statusFrame.height - statusIconImage.size.height) / 2.0) + 1.0), size: statusIconImage.size)
} else {
if let statusIconNode = strongSelf.statusIconNode {
strongSelf.statusIconNode = nil
statusIconNode.removeFromSupernode()
}
}
if let credibilityIcon = credibilityIcon {
let animationCache = item.context.animationCache
let animationRenderer = item.context.animationRenderer
@ -1334,6 +1378,9 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
var statusFrame = self.statusNode.frame
let previousStatusFrame = statusFrame
statusFrame.origin.x = leftInset + offset
if let statusIconImage = self.statusIconNode?.image {
statusFrame.origin.x += statusIconImage.size.width + 1.0
}
self.statusNode.frame = statusFrame
transition.animatePositionAdditive(node: self.statusNode, offset: CGPoint(x: previousStatusFrame.minX - statusFrame.minX, y: 0))

View File

@ -103,6 +103,7 @@ public final class ContextMenuActionItem {
public let icon: (PresentationTheme) -> UIImage?
public let iconSource: ContextMenuActionItemIconSource?
public let textIcon: (PresentationTheme) -> UIImage?
public let textLinkAction: () -> Void
public let action: ((Action) -> Void)?
convenience public init(
@ -116,6 +117,7 @@ public final class ContextMenuActionItem {
icon: @escaping (PresentationTheme) -> UIImage?,
iconSource: ContextMenuActionItemIconSource? = nil,
textIcon: @escaping (PresentationTheme) -> UIImage? = { _ in return nil },
textLinkAction: @escaping () -> Void = {},
action: ((ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void)?
) {
self.init(
@ -129,6 +131,7 @@ public final class ContextMenuActionItem {
icon: icon,
iconSource: iconSource,
textIcon: textIcon,
textLinkAction: textLinkAction,
action: action.flatMap { action in
return { impl in
action(impl.controller, impl.dismissWithResult)
@ -148,6 +151,7 @@ public final class ContextMenuActionItem {
icon: @escaping (PresentationTheme) -> UIImage?,
iconSource: ContextMenuActionItemIconSource? = nil,
textIcon: @escaping (PresentationTheme) -> UIImage? = { _ in return nil },
textLinkAction: @escaping () -> Void = {},
action: ((Action) -> Void)?
) {
self.id = id
@ -160,6 +164,7 @@ public final class ContextMenuActionItem {
self.icon = icon
self.iconSource = iconSource
self.textIcon = textIcon
self.textLinkAction = textLinkAction
self.action = action
}
}

View File

@ -85,7 +85,6 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin
self.titleLabelNode = ImmediateTextNode()
self.titleLabelNode.isAccessibilityElement = false
self.titleLabelNode.displaysAsynchronously = false
self.titleLabelNode.isUserInteractionEnabled = false
self.subtitleNode = ImmediateTextNode()
self.subtitleNode.isAccessibilityElement = false
@ -160,6 +159,16 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin
self.pressed()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.titleLabelNode.tapAttributeAction != nil {
if let result = self.titleLabelNode.hitTest(self.view.convert(point, to: self.titleLabelNode.view), with: event) {
return result
}
}
return super.hitTest(point, with: event)
}
func update(presentationData: PresentationData, constrainedSize: CGSize) -> (minSize: CGSize, apply: (_ size: CGSize, _ transition: ContainedViewLayoutTransition) -> Void) {
let sideInset: CGFloat = 16.0
let verticalInset: CGFloat = 11.0
@ -219,10 +228,23 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin
body: MarkdownAttributeSet(font: titleFont, textColor: titleColor),
bold: MarkdownAttributeSet(font: titleBoldFont, textColor: titleColor),
link: MarkdownAttributeSet(font: titleBoldFont, textColor: presentationData.theme.list.itemAccentColor),
linkAttribute: { _ in return nil }
linkAttribute: { value in return ("URL", value) }
)
)
self.titleLabelNode.attributedText = attributedText
self.titleLabelNode.linkHighlightColor = presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.5)
self.titleLabelNode.highlightAttributeAction = { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
return NSAttributedString.Key(rawValue: "URL")
} else {
return nil
}
}
self.titleLabelNode.tapAttributeAction = { [weak item] attributes, _ in
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
item?.textLinkAction()
}
}
} else {
self.titleLabelNode.attributedText = NSAttributedString(
string: self.item.text,
@ -230,6 +252,8 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin
textColor: titleColor)
}
self.titleLabelNode.isUserInteractionEnabled = self.titleLabelNode.tapAttributeAction != nil
self.subtitleNode.attributedText = subtitle.flatMap { subtitle in
return NSAttributedString(
string: subtitle,

View File

@ -626,7 +626,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
}
case let .reference(reference):
if let transitionInfo = reference.transitionInfo() {
contentRect = convertFrame(transitionInfo.referenceView.bounds, from: transitionInfo.referenceView, to: self.view).insetBy(dx: -2.0, dy: 0.0)
contentRect = convertFrame(transitionInfo.referenceView.bounds.inset(by: transitionInfo.insets), from: transitionInfo.referenceView, to: self.view).insetBy(dx: -2.0, dy: 0.0)
contentRect.size.width += 5.0
contentParentGlobalFrame = CGRect(origin: CGPoint(x: 0.0, y: contentRect.minX), size: CGSize(width: layout.size.width, height: contentRect.height))
} else {
@ -1040,7 +1040,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
self.clippingNode.layer.animateFrame(from: CGRect(origin: CGPoint(), size: layout.size), to: CGRect(origin: CGPoint(x: 0.0, y: putBackInfo.contentAreaInScreenSpace.minY), size: CGSize(width: layout.size.width, height: putBackInfo.contentAreaInScreenSpace.height)), duration: duration, timingFunction: timingFunction, removeOnCompletion: false)
self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: putBackInfo.contentAreaInScreenSpace.minY, duration: duration, timingFunction: timingFunction, removeOnCompletion: false)
currentContentScreenFrame = convertFrame(putBackInfo.referenceView.bounds, from: putBackInfo.referenceView, to: self.view)
currentContentScreenFrame = convertFrame(putBackInfo.referenceView.bounds.inset(by: putBackInfo.insets), from: putBackInfo.referenceView, to: self.view)
} else {
return
}

View File

@ -13,6 +13,7 @@ public enum ItemListDisclosureItemTitleColor {
public enum ItemListDisclosureStyle {
case arrow
case optionArrows
case none
}
@ -116,7 +117,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
let iconNode: ASImageNode
let titleNode: TextNode
let labelNode: TextNode
public let labelNode: TextNode
let arrowNode: ASImageNode
let labelBadgeNode: ASImageNode
let labelImageNode: ASImageNode
@ -198,6 +199,17 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
}
}
public func updateHasContextMenu(hasContextMenu: Bool) {
let transition: ContainedViewLayoutTransition
if hasContextMenu {
transition = .immediate
} else {
transition = .animated(duration: 0.3, curve: .easeInOut)
}
transition.updateAlpha(node: self.labelNode, alpha: hasContextMenu ? 0.5 : 1.0)
transition.updateAlpha(node: self.arrowNode, alpha: hasContextMenu ? 0.5 : 1.0)
}
public func asyncLayout() -> (_ item: ItemListDisclosureItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeLabelLayout = TextNode.asyncLayout(self.labelNode)
@ -213,6 +225,8 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
rightInset = 16.0 + params.rightInset
case .arrow:
rightInset = 34.0 + params.rightInset
case .optionArrows:
rightInset = 34.0 + params.rightInset
}
var updateArrowImage: UIImage?
@ -245,7 +259,12 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
let badgeDiameter: CGFloat = 20.0
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme)
switch item.disclosureStyle {
case .none, .arrow:
updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme)
case .optionArrows:
updateArrowImage = PresentationResourcesItemList.disclosureOptionArrowsImage(item.presentationData.theme)
}
if let badgeColor = badgeColor {
updatedLabelBadgeImage = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: badgeColor)
}
@ -512,14 +531,21 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
}
if let arrowImage = strongSelf.arrowNode.image {
strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - 7.0 - arrowImage.size.width, y: floorToScreenPixels((height - arrowImage.size.height) / 2.0)), size: arrowImage.size)
let arrowRightOffset: CGFloat
switch item.disclosureStyle {
case .optionArrows:
arrowRightOffset = 18.0
case .none, .arrow:
arrowRightOffset = 7.0
}
strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - arrowRightOffset - arrowImage.size.width, y: floorToScreenPixels((height - arrowImage.size.height) / 2.0)), size: arrowImage.size)
}
switch item.disclosureStyle {
case .none:
strongSelf.arrowNode.isHidden = true
case .arrow:
strongSelf.arrowNode.isHidden = false
case .none:
strongSelf.arrowNode.isHidden = true
case .arrow, .optionArrows:
strongSelf.arrowNode.isHidden = false
}
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: height + UIScreenPixel))

View File

@ -659,10 +659,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
hasTimer = true
}
var hasSchedule = true
if controller.chatLocation?.threadId != nil {
hasSchedule = false
}
let hasSchedule = true
self.openingMedia = true

View File

@ -159,7 +159,7 @@ private final class ChannelMembersSearchEntry: Comparable, Identifiable {
case let .participant(participant, label, revealActions, revealed, enabled):
let status: ContactsPeerItemStatus
if let label = label {
status = .custom(string: label, multiline: false)
status = .custom(string: label, multiline: false, isActive: false, icon: nil)
} else if let presence = participant.presences[participant.peer.id], self.addIcon {
status = .presence(EnginePeer.Presence(presence), dateTimeFormat)
} else {

View File

@ -130,7 +130,7 @@ private enum ChannelMembersSearchEntry: Comparable, Identifiable {
case let .peer(_, participant, editing, label, enabled, isChannel, isContact):
let status: ContactsPeerItemStatus
if let label = label {
status = .custom(string: label, multiline: false)
status = .custom(string: label, multiline: false, isActive: false, icon: nil)
} else if participant.peer.id != context.account.peerId {
let presence = participant.presences[participant.peer.id] ?? TelegramUserPresence(status: .none, lastActivity: 0)
status = .presence(EnginePeer.Presence(presence), presentationData.dateTimeFormat)

View File

@ -138,10 +138,10 @@ class ItemListSecretChatKeyItemNode: ListViewItemNode {
return { item, params, neighbors in
let rightInset: CGFloat
switch item.disclosureStyle {
case .none:
rightInset = 16.0 + params.rightInset
case .arrow:
rightInset = 34.0 + params.rightInset
case .none:
rightInset = 16.0 + params.rightInset
case .arrow, .optionArrows:
rightInset = 34.0 + params.rightInset
}
var updateArrowImage: UIImage?
@ -279,7 +279,7 @@ class ItemListSecretChatKeyItemNode: ListViewItemNode {
switch item.disclosureStyle {
case .none:
strongSelf.arrowNode.isHidden = true
case .arrow:
case .arrow, .optionArrows:
strongSelf.arrowNode.isHidden = false
}

View File

@ -173,7 +173,7 @@ private enum OldChannelsEntry: ItemListNodeEntry {
case let .peersHeader(title):
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
case let .peer(_, peer, selected):
return ContactsPeerItem(presentationData: presentationData, style: .blocks, sectionId: self.section, sortOrder: .firstLast, displayOrder: .firstLast, context: arguments.context, peerMode: .peer, peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .custom(string: localizedOldChannelDate(peer: peer, strings: presentationData.strings), multiline: false), badge: nil, enabled: true, selection: ContactsPeerItemSelection.selectable(selected: selected), editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), options: [], actionIcon: .none, index: nil, header: nil, action: { _ in
return ContactsPeerItem(presentationData: presentationData, style: .blocks, sectionId: self.section, sortOrder: .firstLast, displayOrder: .firstLast, context: arguments.context, peerMode: .peer, peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .custom(string: localizedOldChannelDate(peer: peer, strings: presentationData.strings), multiline: false, isActive: false, icon: nil), badge: nil, enabled: true, selection: ContactsPeerItemSelection.selectable(selected: selected), editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), options: [], actionIcon: .none, index: nil, header: nil, action: { _ in
arguments.togglePeer(peer.peer.id, true)
}, setPeerIdWithRevealedOptions: nil, deletePeer: nil, itemHighlighting: nil, contextAction: nil)
}

View File

@ -141,7 +141,7 @@ private enum OldChannelsSearchEntry: Comparable, Identifiable {
func item(context: AccountContext, presentationData: ItemListPresentationData, interaction: OldChannelsSearchInteraction) -> ListViewItem {
switch self {
case let .peer(_, peer, selected):
return ContactsPeerItem(presentationData: presentationData, style: .plain, sortOrder: .firstLast, displayOrder: .firstLast, context: context, peerMode: .peer, peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .custom(string: localizedOldChannelDate(peer: peer, strings: presentationData.strings), multiline: false), badge: nil, enabled: true, selection: ContactsPeerItemSelection.selectable(selected: selected), editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), options: [], actionIcon: .none, index: nil, header: nil, action: { _ in
return ContactsPeerItem(presentationData: presentationData, style: .plain, sortOrder: .firstLast, displayOrder: .firstLast, context: context, peerMode: .peer, peer: .peer(peer: EnginePeer(peer.peer), chatPeer: EnginePeer(peer.peer)), status: .custom(string: localizedOldChannelDate(peer: peer, strings: presentationData.strings), multiline: false, isActive: false, icon: nil), badge: nil, enabled: true, selection: ContactsPeerItemSelection.selectable(selected: selected), editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), options: [], actionIcon: .none, index: nil, header: nil, action: { _ in
interaction.togglePeer(peer.peer.id)
}, setPeerIdWithRevealedOptions: nil, deletePeer: nil, itemHighlighting: nil, contextAction: nil)
}

View File

@ -111,12 +111,12 @@ public struct ChatListForumTopicData: Equatable {
}
public enum ChatListEntry: Comparable {
case MessageEntry(index: ChatListIndex, messages: [Message], readState: ChatListViewReadState?, isRemovedFromTotalUnreadCount: Bool, embeddedInterfaceState: StoredPeerChatInterfaceState?, renderedPeer: RenderedPeer, presence: PeerPresence?, summaryInfo: [ChatListEntryMessageTagSummaryKey: ChatListMessageTagSummaryInfo], forumTopicData: ChatListForumTopicData?, topForumTopics: [ChatListForumTopicData], hasFailed: Bool, isContact: Bool)
case MessageEntry(index: ChatListIndex, messages: [Message], readState: ChatListViewReadState?, isRemovedFromTotalUnreadCount: Bool, embeddedInterfaceState: StoredPeerChatInterfaceState?, renderedPeer: RenderedPeer, presence: PeerPresence?, summaryInfo: [ChatListEntryMessageTagSummaryKey: ChatListMessageTagSummaryInfo], forumTopicData: ChatListForumTopicData?, topForumTopics: [ChatListForumTopicData], hasFailed: Bool, isContact: Bool, autoremoveTimeout: Int32?)
case HoleEntry(ChatListHole)
public var index: ChatListIndex {
switch self {
case let .MessageEntry(index, _, _, _, _, _, _, _, _, _, _, _):
case let .MessageEntry(index, _, _, _, _, _, _, _, _, _, _, _, _):
return index
case let .HoleEntry(hole):
return ChatListIndex(pinningIndex: nil, messageIndex: hole.index)
@ -125,9 +125,9 @@ public enum ChatListEntry: Comparable {
public static func ==(lhs: ChatListEntry, rhs: ChatListEntry) -> Bool {
switch lhs {
case let .MessageEntry(lhsIndex, lhsMessages, lhsReadState, lhsIsRemovedFromTotalUnreadCount, lhsEmbeddedState, lhsPeer, lhsPresence, lhsInfo, lhsForumTopicData, lhsTopForumTopics, lhsHasFailed, lhsIsContact):
case let .MessageEntry(lhsIndex, lhsMessages, lhsReadState, lhsIsRemovedFromTotalUnreadCount, lhsEmbeddedState, lhsPeer, lhsPresence, lhsInfo, lhsForumTopicData, lhsTopForumTopics, lhsHasFailed, lhsIsContact, lhsAutoremoveTimeout):
switch rhs {
case let .MessageEntry(rhsIndex, rhsMessages, rhsReadState, rhsIsRemovedFromTotalUnreadCount, rhsEmbeddedState, rhsPeer, rhsPresence, rhsInfo, rhsForumTopicData, rhsTopForumTopics, rhsHasFailed, rhsIsContact):
case let .MessageEntry(rhsIndex, rhsMessages, rhsReadState, rhsIsRemovedFromTotalUnreadCount, rhsEmbeddedState, rhsPeer, rhsPresence, rhsInfo, rhsForumTopicData, rhsTopForumTopics, rhsHasFailed, rhsIsContact, rhsAutoremoveTimeout):
if lhsIndex != rhsIndex {
return false
}
@ -177,6 +177,9 @@ public enum ChatListEntry: Comparable {
if lhsIsContact != rhsIsContact {
return false
}
if lhsAutoremoveTimeout != rhsAutoremoveTimeout {
return false
}
return true
default:
return false
@ -197,7 +200,7 @@ public enum ChatListEntry: Comparable {
enum MutableChatListEntry: Equatable {
case IntermediateMessageEntry(index: ChatListIndex, messageIndex: MessageIndex?)
case MessageEntry(index: ChatListIndex, messages: [Message], readState: ChatListViewReadState?, notificationSettings: PeerNotificationSettings?, isRemovedFromTotalUnreadCount: Bool, embeddedInterfaceState: StoredPeerChatInterfaceState?, renderedPeer: RenderedPeer, presence: PeerPresence?, tagSummaryInfo: [ChatListEntryMessageTagSummaryKey: ChatListMessageTagSummaryInfo], forumTopicData: ChatListForumTopicData?, topForumTopics: [ChatListForumTopicData], hasFailedMessages: Bool, isContact: Bool)
case MessageEntry(index: ChatListIndex, messages: [Message], readState: ChatListViewReadState?, notificationSettings: PeerNotificationSettings?, isRemovedFromTotalUnreadCount: Bool, embeddedInterfaceState: StoredPeerChatInterfaceState?, renderedPeer: RenderedPeer, presence: PeerPresence?, tagSummaryInfo: [ChatListEntryMessageTagSummaryKey: ChatListMessageTagSummaryInfo], forumTopicData: ChatListForumTopicData?, topForumTopics: [ChatListForumTopicData], hasFailedMessages: Bool, isContact: Bool, autoremoveTimeout: Int32?)
case HoleEntry(ChatListHole)
init(_ intermediateEntry: ChatListIntermediateEntry, cachedDataTable: CachedPeerDataTable, readStateTable: MessageHistoryReadStateTable, messageHistoryTable: MessageHistoryTable) {
@ -213,7 +216,7 @@ enum MutableChatListEntry: Equatable {
switch self {
case let .IntermediateMessageEntry(index, _):
return index
case let .MessageEntry(index, _, _, _, _, _, _, _, _, _, _, _, _):
case let .MessageEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _):
return index
case let .HoleEntry(hole):
return ChatListIndex(pinningIndex: nil, messageIndex: hole.index)
@ -713,7 +716,12 @@ final class MutableChatListView {
}
}
return .MessageEntry(index: index, messages: renderedMessages, readState: readState, notificationSettings: notificationSettings, isRemovedFromTotalUnreadCount: false, embeddedInterfaceState: postbox.peerChatInterfaceStateTable.get(index.messageIndex.id.peerId), renderedPeer: renderedPeer, presence: presence, tagSummaryInfo: [:], forumTopicData: forumTopicData, topForumTopics: topForumTopics, hasFailedMessages: postbox.messageHistoryFailedTable.contains(peerId: index.messageIndex.id.peerId), isContact: isContact)
var autoremoveTimeout: Int32?
if let cachedData = postbox.cachedPeerDataTable.get(index.messageIndex.id.peerId) {
autoremoveTimeout = postbox.seedConfiguration.decodeAutoremoveTimeout(cachedData)
}
return .MessageEntry(index: index, messages: renderedMessages, readState: readState, notificationSettings: notificationSettings, isRemovedFromTotalUnreadCount: false, embeddedInterfaceState: postbox.peerChatInterfaceStateTable.get(index.messageIndex.id.peerId), renderedPeer: renderedPeer, presence: presence, tagSummaryInfo: [:], forumTopicData: forumTopicData, topForumTopics: topForumTopics, hasFailedMessages: postbox.messageHistoryFailedTable.contains(peerId: index.messageIndex.id.peerId), isContact: isContact, autoremoveTimeout: autoremoveTimeout)
default:
return nil
}
@ -742,8 +750,8 @@ public final class ChatListView {
var entries: [ChatListEntry] = []
for entry in mutableView.sampledState.entries {
switch entry {
case let .MessageEntry(index, messages, combinedReadState, _, isRemovedFromTotalUnreadCount, embeddedState, peer, peerPresence, summaryInfo, forumTopicData, topForumTopics, hasFailed, isContact):
entries.append(.MessageEntry(index: index, messages: messages, readState: combinedReadState, isRemovedFromTotalUnreadCount: isRemovedFromTotalUnreadCount, embeddedInterfaceState: embeddedState, renderedPeer: peer, presence: peerPresence, summaryInfo: summaryInfo, forumTopicData: forumTopicData, topForumTopics: topForumTopics, hasFailed: hasFailed, isContact: isContact))
case let .MessageEntry(index, messages, combinedReadState, _, isRemovedFromTotalUnreadCount, embeddedState, peer, peerPresence, summaryInfo, forumTopicData, topForumTopics, hasFailed, isContact, autoremoveTimeout):
entries.append(.MessageEntry(index: index, messages: messages, readState: combinedReadState, isRemovedFromTotalUnreadCount: isRemovedFromTotalUnreadCount, embeddedInterfaceState: embeddedState, renderedPeer: peer, presence: peerPresence, summaryInfo: summaryInfo, forumTopicData: forumTopicData, topForumTopics: topForumTopics, hasFailed: hasFailed, isContact: isContact, autoremoveTimeout: autoremoveTimeout))
case let .HoleEntry(hole):
entries.append(.HoleEntry(hole))
case .IntermediateMessageEntry:
@ -760,9 +768,9 @@ public final class ChatListView {
var additionalItemEntries: [ChatListAdditionalItemEntry] = []
for entry in mutableView.additionalItemEntries {
switch entry.entry {
case let .MessageEntry(index, messages, combinedReadState, _, isExcludedFromUnreadCount, embeddedState, peer, peerPresence, summaryInfo, forumTopicData, topForumTopics, hasFailed, isContact):
case let .MessageEntry(index, messages, combinedReadState, _, isExcludedFromUnreadCount, embeddedState, peer, peerPresence, summaryInfo, forumTopicData, topForumTopics, hasFailed, isContact, autoremoveTimeout):
additionalItemEntries.append(ChatListAdditionalItemEntry(
entry: .MessageEntry(index: index, messages: messages, readState: combinedReadState, isRemovedFromTotalUnreadCount: isExcludedFromUnreadCount, embeddedInterfaceState: embeddedState, renderedPeer: peer, presence: peerPresence, summaryInfo: summaryInfo, forumTopicData: forumTopicData, topForumTopics: topForumTopics, hasFailed: hasFailed, isContact: isContact),
entry: .MessageEntry(index: index, messages: messages, readState: combinedReadState, isRemovedFromTotalUnreadCount: isExcludedFromUnreadCount, embeddedInterfaceState: embeddedState, renderedPeer: peer, presence: peerPresence, summaryInfo: summaryInfo, forumTopicData: forumTopicData, topForumTopics: topForumTopics, hasFailed: hasFailed, isContact: isContact, autoremoveTimeout: autoremoveTimeout),
info: entry.info
))
case .HoleEntry:

View File

@ -515,7 +515,7 @@ private final class ChatListViewSpaceState {
let entryPeer: Peer
let entryNotificationsPeerId: PeerId
switch entry {
case let .MessageEntry(_, _, _, _, _, _, renderedPeer, _, _, _, _, _, _):
case let .MessageEntry(_, _, _, _, _, _, renderedPeer, _, _, _, _, _, _, _):
if let peer = renderedPeer.peer {
entryPeer = peer
entryNotificationsPeerId = peer.notificationSettingsPeerId ?? peer.id
@ -630,13 +630,13 @@ private final class ChatListViewSpaceState {
if self.orderedEntries.mutableScan({ entry in
switch entry {
case let .MessageEntry(index, messages, readState, _, _, embeddedInterfaceState, renderedPeer, presence, tagSummaryInfo, forumTopicData, topForumTopics, hasFailedMessages, isContact):
case let .MessageEntry(index, messages, readState, _, _, embeddedInterfaceState, renderedPeer, presence, tagSummaryInfo, forumTopicData, topForumTopics, hasFailedMessages, isContact, autoremoveTimeout):
if let peer = renderedPeer.peer {
let notificationsPeerId = peer.notificationSettingsPeerId ?? peer.id
if let (_, updated) = transaction.currentUpdatedPeerNotificationSettings[notificationsPeerId] {
let isRemovedFromTotalUnreadCount = resolvedIsRemovedFromTotalUnreadCount(globalSettings: globalNotificationSettings, peer: peer, peerSettings: updated)
return .MessageEntry(index: index, messages: messages, readState: readState, notificationSettings: updated, isRemovedFromTotalUnreadCount: isRemovedFromTotalUnreadCount, embeddedInterfaceState: embeddedInterfaceState, renderedPeer: renderedPeer, presence: presence, tagSummaryInfo: tagSummaryInfo, forumTopicData: forumTopicData, topForumTopics: topForumTopics, hasFailedMessages: hasFailedMessages, isContact: isContact)
return .MessageEntry(index: index, messages: messages, readState: readState, notificationSettings: updated, isRemovedFromTotalUnreadCount: isRemovedFromTotalUnreadCount, embeddedInterfaceState: embeddedInterfaceState, renderedPeer: renderedPeer, presence: presence, tagSummaryInfo: tagSummaryInfo, forumTopicData: forumTopicData, topForumTopics: topForumTopics, hasFailedMessages: hasFailedMessages, isContact: isContact, autoremoveTimeout: autoremoveTimeout)
} else {
return nil
}
@ -654,7 +654,7 @@ private final class ChatListViewSpaceState {
if !transaction.updatedFailedMessagePeerIds.isEmpty {
if self.orderedEntries.mutableScan({ entry in
switch entry {
case let .MessageEntry(index, messages, readState, notificationSettings, isRemovedFromTotalUnreadCount, embeddedInterfaceState, renderedPeer, presence, tagSummaryInfo, forumTopicData, topForumTopics, _, isContact):
case let .MessageEntry(index, messages, readState, notificationSettings, isRemovedFromTotalUnreadCount, embeddedInterfaceState, renderedPeer, presence, tagSummaryInfo, forumTopicData, topForumTopics, _, isContact, autoremoveTimeout):
if transaction.updatedFailedMessagePeerIds.contains(index.messageIndex.id.peerId) {
return .MessageEntry(
index: index,
@ -669,7 +669,8 @@ private final class ChatListViewSpaceState {
forumTopicData: forumTopicData,
topForumTopics: topForumTopics,
hasFailedMessages: postbox.messageHistoryFailedTable.contains(peerId: index.messageIndex.id.peerId),
isContact: isContact
isContact: isContact,
autoremoveTimeout: autoremoveTimeout
)
} else {
return nil
@ -685,7 +686,7 @@ private final class ChatListViewSpaceState {
if !transaction.currentUpdatedPeers.isEmpty {
if self.orderedEntries.mutableScan({ entry in
switch entry {
case let .MessageEntry(index, messages, readState, notificationSettings, isRemovedFromTotalUnreadCount, embeddedInterfaceState, entryRenderedPeer, presence, tagSummaryInfo, forumTopicData, topForumTopics, hasFailedMessages, isContact):
case let .MessageEntry(index, messages, readState, notificationSettings, isRemovedFromTotalUnreadCount, embeddedInterfaceState, entryRenderedPeer, presence, tagSummaryInfo, forumTopicData, topForumTopics, hasFailedMessages, isContact, autoremoveTimeout):
var updatedMessages: [Message] = messages
var hasUpdatedMessages = false
for i in 0 ..< updatedMessages.count {
@ -710,7 +711,9 @@ private final class ChatListViewSpaceState {
forumTopicData: forumTopicData,
topForumTopics: topForumTopics,
hasFailedMessages: hasFailedMessages,
isContact: isContact)
isContact: isContact,
autoremoveTimeout: autoremoveTimeout
)
} else {
return nil
}
@ -725,7 +728,7 @@ private final class ChatListViewSpaceState {
if !transaction.currentUpdatedPeerPresences.isEmpty {
if self.orderedEntries.mutableScan({ entry in
switch entry {
case let .MessageEntry(index, messages, readState, notificationSettings, isRemovedFromTotalUnreadCount, embeddedInterfaceState, entryRenderedPeer, _, tagSummaryInfo, forumTopicData, topForumTopics, hasFailedMessages, isContact):
case let .MessageEntry(index, messages, readState, notificationSettings, isRemovedFromTotalUnreadCount, embeddedInterfaceState, entryRenderedPeer, _, tagSummaryInfo, forumTopicData, topForumTopics, hasFailedMessages, isContact, autoremoveTimeout):
var presencePeerId = entryRenderedPeer.peerId
if let peer = entryRenderedPeer.peers[entryRenderedPeer.peerId], let associatedPeerId = peer.associatedPeerId {
presencePeerId = associatedPeerId
@ -744,7 +747,8 @@ private final class ChatListViewSpaceState {
forumTopicData: forumTopicData,
topForumTopics: topForumTopics,
hasFailedMessages: hasFailedMessages,
isContact: isContact
isContact: isContact,
autoremoveTimeout: autoremoveTimeout
)
} else {
return nil
@ -763,7 +767,7 @@ private final class ChatListViewSpaceState {
let entryPeer: Peer
let entryNotificationsPeerId: PeerId
switch entry {
case let .MessageEntry(_, _, _, _, _, _, entryRenderedPeer, _, _, _, _, _, _):
case let .MessageEntry(_, _, _, _, _, _, entryRenderedPeer, _, _, _, _, _, _, _):
if let peer = entryRenderedPeer.peer {
entryPeer = peer
entryNotificationsPeerId = peer.notificationSettingsPeerId ?? peer.id
@ -881,10 +885,20 @@ private final class ChatListViewSpaceState {
}
}
if !transaction.currentUpdatedMessageTagSummaries.isEmpty || !transaction.currentUpdatedMessageActionsSummaries.isEmpty || !transaction.updatedPeerThreadsSummaries.isEmpty {
var cachedPeerDataUpdated = false
if !transaction.currentUpdatedCachedPeerData.isEmpty {
let _ = self.orderedEntries.mutableScan { entry in
if transaction.currentUpdatedCachedPeerData[entry.index.messageIndex.id.peerId] != nil {
cachedPeerDataUpdated = true
}
return nil
}
}
if !transaction.currentUpdatedMessageTagSummaries.isEmpty || !transaction.currentUpdatedMessageActionsSummaries.isEmpty || !transaction.updatedPeerThreadsSummaries.isEmpty || cachedPeerDataUpdated {
if self.orderedEntries.mutableScan({ entry in
switch entry {
case let .MessageEntry(index, messages, readState, notificationSettings, isRemovedFromTotalUnreadCount, embeddedInterfaceState, entryRenderedPeer, presence, tagSummaryInfo, forumTopicData, topForumTopics, hasFailedMessages, isContact):
case let .MessageEntry(index, messages, readState, notificationSettings, isRemovedFromTotalUnreadCount, embeddedInterfaceState, entryRenderedPeer, presence, tagSummaryInfo, forumTopicData, topForumTopics, hasFailedMessages, isContact, autoremoveTimeout):
var updatedChatListMessageTagSummaryInfo: [ChatListEntryMessageTagSummaryKey: ChatListMessageTagSummaryInfo] = tagSummaryInfo
var didUpdateSummaryInfo = false
@ -938,6 +952,14 @@ private final class ChatListViewSpaceState {
didUpdateSummaryInfo = true
}
var updatedAutoremoveTimeout: Int32?
if let cachedData = postbox.cachedPeerDataTable.get(index.messageIndex.id.peerId) {
updatedAutoremoveTimeout = postbox.seedConfiguration.decodeAutoremoveTimeout(cachedData)
if updatedAutoremoveTimeout != autoremoveTimeout {
didUpdateSummaryInfo = true
}
}
if didUpdateSummaryInfo {
return .MessageEntry(
index: index,
@ -952,7 +974,8 @@ private final class ChatListViewSpaceState {
forumTopicData: forumTopicData,
topForumTopics: topForumTopics,
hasFailedMessages: hasFailedMessages,
isContact: isContact
isContact: isContact,
autoremoveTimeout: updatedAutoremoveTimeout
)
} else {
return nil
@ -1086,7 +1109,7 @@ private extension MutableChatListEntry {
switch self {
case let .IntermediateMessageEntry(index, _):
return index.messageIndex.id.peerId
case let .MessageEntry(index, _, _, _, _, _, _, _, _, _, _, _, _):
case let .MessageEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _):
return index.messageIndex.id.peerId
case .HoleEntry:
return nil
@ -1097,7 +1120,7 @@ private extension MutableChatListEntry {
switch self {
case let .IntermediateMessageEntry(index, _):
return MutableChatListEntryIndex(index: index, isMessage: true)
case let .MessageEntry(index, _, _, _, _, _, _, _, _, _, _, _, _):
case let .MessageEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _):
return MutableChatListEntryIndex(index: index, isMessage: true)
case let .HoleEntry(hole):
return MutableChatListEntryIndex(index: ChatListIndex(pinningIndex: nil, messageIndex: hole.index), isMessage: false)
@ -1108,7 +1131,7 @@ private extension MutableChatListEntry {
switch self {
case let .IntermediateMessageEntry(index, _):
return .peer(index.messageIndex.id.peerId)
case let .MessageEntry(index, _, _, _, _, _, _, _, _, _, _, _, _):
case let .MessageEntry(index, _, _, _, _, _, _, _, _, _, _, _, _, _):
return .peer(index.messageIndex.id.peerId)
case let .HoleEntry(hole):
return .hole(hole.index)
@ -1558,7 +1581,12 @@ struct ChatListViewState {
}
}
let updatedEntry: MutableChatListEntry = .MessageEntry(index: index, messages: renderedMessages, readState: readState, notificationSettings: notificationSettings, isRemovedFromTotalUnreadCount: isRemovedFromTotalUnreadCount, embeddedInterfaceState: postbox.peerChatInterfaceStateTable.get(index.messageIndex.id.peerId), renderedPeer: renderedPeer, presence: presence, tagSummaryInfo: tagSummaryInfo, forumTopicData: forumTopicData, topForumTopics: topForumTopics, hasFailedMessages: false, isContact: postbox.contactsTable.isContact(peerId: index.messageIndex.id.peerId))
var autoremoveTimeout: Int32?
if let cachedData = postbox.cachedPeerDataTable.get(index.messageIndex.id.peerId) {
autoremoveTimeout = postbox.seedConfiguration.decodeAutoremoveTimeout(cachedData)
}
let updatedEntry: MutableChatListEntry = .MessageEntry(index: index, messages: renderedMessages, readState: readState, notificationSettings: notificationSettings, isRemovedFromTotalUnreadCount: isRemovedFromTotalUnreadCount, embeddedInterfaceState: postbox.peerChatInterfaceStateTable.get(index.messageIndex.id.peerId), renderedPeer: renderedPeer, presence: presence, tagSummaryInfo: tagSummaryInfo, forumTopicData: forumTopicData, topForumTopics: topForumTopics, hasFailedMessages: false, isContact: postbox.contactsTable.isContact(peerId: index.messageIndex.id.peerId), autoremoveTimeout: autoremoveTimeout)
if directionIndex == 0 {
self.stateBySpace[space]!.orderedEntries.setLowerOrAtAnchorAtArrayIndex(listIndex, to: updatedEntry)
} else {

View File

@ -1,23 +1,58 @@
final class MutableInvalidatedMessageHistoryTagSummariesView: MutablePostboxView {
private let peerId: PeerId?
private let threadId: Int64?
private let namespace: MessageId.Namespace
private let tagMask: MessageTags
var entries = Set<InvalidatedMessageHistoryTagsSummaryEntry>()
init(postbox: PostboxImpl, tagMask: MessageTags, namespace: MessageId.Namespace) {
init(postbox: PostboxImpl, peerId: PeerId?, threadId: Int64?, tagMask: MessageTags, namespace: MessageId.Namespace) {
self.peerId = peerId
self.threadId = threadId
self.tagMask = tagMask
self.namespace = namespace
for entry in postbox.invalidatedMessageHistoryTagsSummaryTable.get(tagMask: tagMask, namespace: namespace) {
self.entries.insert(entry)
if let peerId = self.peerId {
if let entry = postbox.invalidatedMessageHistoryTagsSummaryTable.get(peerId: peerId, threadId: self.threadId, tagMask: self.tagMask, namespace: self.namespace) {
self.entries.insert(entry)
}
} else {
for entry in postbox.invalidatedMessageHistoryTagsSummaryTable.get(tagMask: tagMask, namespace: namespace) {
self.entries.insert(entry)
}
}
}
func replay(postbox: PostboxImpl, transaction: PostboxTransaction) -> Bool {
var updated = false
for operation in transaction.currentInvalidateMessageTagSummaries {
switch operation {
if let peerId = self.peerId {
var maybeUpdated = false
loop: for operation in transaction.currentInvalidateMessageTagSummaries {
switch operation {
case let .add(entry):
if entry.key.peerId == peerId && entry.key.threadId == self.threadId {
maybeUpdated = true
break loop
}
case let .remove(key):
if key.peerId == peerId && key.threadId == self.threadId {
maybeUpdated = true
break loop
}
}
}
if maybeUpdated {
self.entries.removeAll()
if let entry = postbox.invalidatedMessageHistoryTagsSummaryTable.get(peerId: peerId, threadId: self.threadId, tagMask: self.tagMask, namespace: self.namespace) {
self.entries.insert(entry)
}
updated = true
}
} else {
for operation in transaction.currentInvalidateMessageTagSummaries {
switch operation {
case let .add(entry):
if entry.key.namespace == self.namespace && entry.key.tagMask == self.tagMask {
self.entries.insert(entry)
@ -33,8 +68,10 @@ final class MutableInvalidatedMessageHistoryTagSummariesView: MutablePostboxView
}
updated = true
}
}
}
}
return updated
}

View File

@ -74,6 +74,10 @@ final class InvalidatedMessageHistoryTagsSummaryTable: Table {
return entries
}
func get(peerId: PeerId, threadId: Int64?, tagMask: MessageTags, namespace: MessageId.Namespace) -> InvalidatedMessageHistoryTagsSummaryEntry? {
return self.get(InvalidatedMessageHistoryTagsSummaryKey(peerId: peerId, namespace: namespace, tagMask: tagMask, threadId: threadId))
}
private func get(_ key: InvalidatedMessageHistoryTagsSummaryKey) -> InvalidatedMessageHistoryTagsSummaryEntry? {
if let value = self.valueBox.get(self.table, key: self.key(key)) {
var version: Int32 = 0

View File

@ -75,6 +75,7 @@ public final class SeedConfiguration {
public let defaultGlobalNotificationSettings: PostboxGlobalNotificationSettings
public let mergeMessageAttributes: ([MessageAttribute], inout [MessageAttribute]) -> Void
public let decodeMessageThreadInfo: (CodableEntry) -> Message.AssociatedThreadInfo?
public let decodeAutoremoveTimeout: (CachedPeerData) -> Int32?
public init(
globalMessageIdsPeerIdNamespaces: Set<GlobalMessageIdsNamespace>,
@ -99,7 +100,8 @@ public final class SeedConfiguration {
getGlobalNotificationSettings: @escaping (Transaction) -> PostboxGlobalNotificationSettings?,
defaultGlobalNotificationSettings: PostboxGlobalNotificationSettings,
mergeMessageAttributes: @escaping ([MessageAttribute], inout [MessageAttribute]) -> Void,
decodeMessageThreadInfo: @escaping (CodableEntry) -> Message.AssociatedThreadInfo?
decodeMessageThreadInfo: @escaping (CodableEntry) -> Message.AssociatedThreadInfo?,
decodeAutoremoveTimeout: @escaping (CachedPeerData) -> Int32?
) {
self.globalMessageIdsPeerIdNamespaces = globalMessageIdsPeerIdNamespaces
self.initializeChatListWithHole = initializeChatListWithHole
@ -120,5 +122,6 @@ public final class SeedConfiguration {
self.defaultGlobalNotificationSettings = defaultGlobalNotificationSettings
self.mergeMessageAttributes = mergeMessageAttributes
self.decodeMessageThreadInfo = decodeMessageThreadInfo
self.decodeAutoremoveTimeout = decodeAutoremoveTimeout
}
}

View File

@ -10,7 +10,7 @@ public enum PostboxViewKey: Hashable {
case globalMessageTags(globalTag: GlobalMessageTags, position: MessageIndex, count: Int, groupingPredicate: ((Message, Message) -> Bool)?)
case peer(peerId: PeerId, components: PeerViewComponents)
case pendingMessageActions(type: PendingMessageActionType)
case invalidatedMessageHistoryTagSummaries(tagMask: MessageTags, namespace: MessageId.Namespace)
case invalidatedMessageHistoryTagSummaries(peerId: PeerId?, threadId: Int64?, tagMask: MessageTags, namespace: MessageId.Namespace)
case pendingMessageActionsSummary(type: PendingMessageActionType, peerId: PeerId, namespace: MessageId.Namespace)
case historyTagSummaryView(tag: MessageTags, peerId: PeerId, threadId: Int64?, namespace: MessageId.Namespace)
case cachedPeerData(peerId: PeerId)
@ -61,7 +61,9 @@ public enum PostboxViewKey: Hashable {
hasher.combine(peerId)
case let .pendingMessageActions(type):
hasher.combine(type)
case let .invalidatedMessageHistoryTagSummaries(tagMask, namespace):
case let .invalidatedMessageHistoryTagSummaries(peerId, threadId, tagMask, namespace):
hasher.combine(peerId)
hasher.combine(threadId)
hasher.combine(tagMask)
hasher.combine(namespace)
case let .pendingMessageActionsSummary(type, peerId, namespace):
@ -191,8 +193,8 @@ public enum PostboxViewKey: Hashable {
} else {
return false
}
case .invalidatedMessageHistoryTagSummaries:
if case .invalidatedMessageHistoryTagSummaries = rhs {
case let .invalidatedMessageHistoryTagSummaries(peerId, threadId, tagMask, namespace):
if case .invalidatedMessageHistoryTagSummaries(peerId, threadId, tagMask, namespace) = rhs {
return true
} else {
return false
@ -395,8 +397,8 @@ func postboxViewForKey(postbox: PostboxImpl, key: PostboxViewKey) -> MutablePost
return MutablePeerView(postbox: postbox, peerId: peerId, components: components)
case let .pendingMessageActions(type):
return MutablePendingMessageActionsView(postbox: postbox, type: type)
case let .invalidatedMessageHistoryTagSummaries(tagMask, namespace):
return MutableInvalidatedMessageHistoryTagSummariesView(postbox: postbox, tagMask: tagMask, namespace: namespace)
case let .invalidatedMessageHistoryTagSummaries(peerId, threadId, tagMask, namespace):
return MutableInvalidatedMessageHistoryTagSummariesView(postbox: postbox, peerId: peerId, threadId: threadId, tagMask: tagMask, namespace: namespace)
case let .pendingMessageActionsSummary(type, peerId, namespace):
return MutablePendingMessageActionsSummaryView(postbox: postbox, type: type, peerId: peerId, namespace: namespace)
case let .historyTagSummaryView(tag, peerId, threadId, namespace):

View File

@ -0,0 +1,109 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import AccountContext
class GlobalAutoremoveHeaderItem: ListViewItem, ItemListItem {
let context: AccountContext
let theme: PresentationTheme
let sectionId: ItemListSectionId
init(context: AccountContext, theme: PresentationTheme, sectionId: ItemListSectionId) {
self.context = context
self.theme = theme
self.sectionId = sectionId
}
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) {
async {
let node = GlobalAutoremoveHeaderItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
guard let nodeValue = node() as? GlobalAutoremoveHeaderItemNode else {
assertionFailure()
return
}
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
private let titleFont = Font.regular(13.0)
class GlobalAutoremoveHeaderItemNode: ListViewItemNode {
private var animationNode: AnimatedStickerNode
private var item: GlobalAutoremoveHeaderItem?
init() {
self.animationNode = DefaultAnimatedStickerNodeImpl()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.animationNode)
}
func asyncLayout() -> (_ item: GlobalAutoremoveHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
return { item, params, neighbors in
//let leftInset: CGFloat = 32.0 + params.leftInset
let topInset: CGFloat = 92.0
let contentSize = CGSize(width: params.width, height: topInset)
let insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
if strongSelf.item == nil {
strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "GlobalAutoRemove"), width: 192, height: 192, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
strongSelf.animationNode.visibility = true
}
strongSelf.item = item
let iconSize = CGSize(width: 96.0, height: 96.0)
strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - iconSize.width) / 2.0), y: -10.0), size: iconSize)
strongSelf.animationNode.updateLayout(size: iconSize)
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}

View File

@ -0,0 +1,407 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import AuthTransferUI
import ItemListPeerActionItem
import DeviceAccess
import QrCodeUI
import ChatTimerScreen
import UndoUI
private final class GlobalAutoremoveScreenArguments {
let context: AccountContext
let updateValue: (Int32) -> Void
let openCustomValue: () -> Void
let infoLinkAction: () -> Void
init(
context: AccountContext,
updateValue: @escaping (Int32) -> Void,
openCustomValue: @escaping () -> Void,
infoLinkAction: @escaping () -> Void
) {
self.context = context
self.updateValue = updateValue
self.openCustomValue = openCustomValue
self.infoLinkAction = infoLinkAction
}
}
private enum GlobalAutoremoveSection: Int32 {
case header
case general
}
private enum GlobalAutoremoveEntry: ItemListNodeEntry {
case header
case sectionHeader(String)
case timerOption(value: Int32, text: String, isSelected: Bool)
case customAction(String)
case info(String)
var section: ItemListSectionId {
switch self {
case .header:
return GlobalAutoremoveSection.header.rawValue
case .sectionHeader, .timerOption, .customAction, .info:
return GlobalAutoremoveSection.general.rawValue
}
}
var stableId: Int {
return self.sortIndex
}
var sortIndex: Int {
switch self {
case .header:
return 0
case .sectionHeader:
return 1
case let .timerOption(value, _, _):
return 1000 + Int(value)
case .customAction:
return Int.max - 1000 + 0
case .info:
return Int.max - 1000 + 1
}
}
static func ==(lhs: GlobalAutoremoveEntry, rhs: GlobalAutoremoveEntry) -> Bool {
switch lhs {
case .header:
if case .header = rhs {
return true
} else {
return false
}
case let .sectionHeader(text):
if case .sectionHeader(text) = rhs {
return true
} else {
return false
}
case let .timerOption(value, text, isSelected):
if case .timerOption(value, text, isSelected) = rhs {
return true
} else {
return false
}
case let .customAction(text):
if case .customAction(text) = rhs {
return true
} else {
return false
}
case let .info(text):
if case .info(text) = rhs {
return true
} else {
return false
}
}
}
static func <(lhs: GlobalAutoremoveEntry, rhs: GlobalAutoremoveEntry) -> Bool {
return lhs.sortIndex < rhs.sortIndex
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! GlobalAutoremoveScreenArguments
switch self {
case .header:
return GlobalAutoremoveHeaderItem(context: arguments.context, theme: presentationData.theme, sectionId: self.section)
case let .sectionHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .timerOption(value, text, isSelected):
return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .right, checked: isSelected, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.updateValue(value)
})
case let .customAction(text):
return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.openCustomValue()
})
case let .info(text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in
arguments.infoLinkAction()
})
}
}
}
private struct GlobalAutoremoveScreenState: Equatable {
var additionalValues: Set<Int32>
var updatedValue: Int32
}
private func globalAutoremoveScreenEntries(presentationData: PresentationData, state: GlobalAutoremoveScreenState) -> [GlobalAutoremoveEntry] {
var entries: [GlobalAutoremoveEntry] = []
entries.append(.header)
let effectiveCurrentValue = state.updatedValue
//TODO:localize
entries.append(.sectionHeader("SELF-DESTRUCT TIMER"))
var values: [Int32] = [
0,
1 * 24 * 60 * 60,
7 * 24 * 60 * 60,
31 * 24 * 60 * 60
]
if !values.contains(effectiveCurrentValue) {
values.append(effectiveCurrentValue)
}
for value in state.additionalValues {
if !values.contains(value) {
values.append(value)
}
}
values.sort()
//TODO:localize
for value in values {
let text: String
if value == 0 {
text = "Off"
} else {
text = "After \(timeIntervalString(strings: presentationData.strings, value: value))"
}
entries.append(.timerOption(value: value, text: text, isSelected: effectiveCurrentValue == value))
}
entries.append(.customAction("Set Custom Time..."))
//TODO:localize
if effectiveCurrentValue == 0 {
entries.append(.info("If enabled, all new messages in chats you start will be automatically deleted for everyone at some point after they have been sent. This will not affect your existing chats."))
} else {
entries.append(.info("All new messages in chats you started will be automatically deleted for everyone 3 weeks after they have been sent. You can also [apply this setting for your existing chats]()."))
}
return entries
}
public func globalAutoremoveScreen(context: AccountContext, initialValue: Int32, updated: @escaping (Int32) -> Void) -> ViewController {
let initialState = GlobalAutoremoveScreenState(
additionalValues: Set([initialValue]),
updatedValue: initialValue
)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
let updateState: ((GlobalAutoremoveScreenState) -> GlobalAutoremoveScreenState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
var dismissImpl: (() -> Void)?
var getController: (() -> ViewController?)?
let _ = dismissImpl
let _ = pushControllerImpl
let _ = presentControllerImpl
let _ = updateState
let actionsDisposable = DisposableSet()
let updateTimeoutDisposable = MetaDisposable()
actionsDisposable.add(updateTimeoutDisposable)
let updateValue: (Int32) -> Void = { timeout in
let apply: (Int32) -> Void = { timeout in
updateState { state in
var state = state
state.updatedValue = timeout
if timeout != 0 {
state.additionalValues.insert(timeout)
}
return state
}
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
var isOn: Bool = true
var text: String?
if timeout != 0 {
text = "Messages in all new chats you start will be automatically deleted after \(timeIntervalString(strings: presentationData.strings, value: timeout))."
} else {
isOn = false
text = "Messages in all new chats you start will not be automatically deleted.";
}
if let text = text {
var animateAsReplacement = false
if let window = getController?()?.window {
window.forEachController { other in
if let other = other as? UndoOverlayController {
animateAsReplacement = true
other.dismiss()
}
}
}
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .autoDelete(isOn: isOn, title: nil, text: text), elevatedLayout: false, animateInAsReplacement: animateAsReplacement, action: { _ in return false }), nil)
}
updateTimeoutDisposable.set((context.engine.privacy.updateGlobalMessageRemovalTimeout(timeout: timeout == 0 ? nil : timeout)
|> deliverOnMainQueue).start(completed: {
updated(timeout)
}))
}
if timeout == 0 || stateValue.with({ $0 }).updatedValue != 0 {
apply(timeout)
} else {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize
let valueText = timeIntervalString(strings: presentationData.strings, value: timeout)
presentControllerImpl?(standardTextAlertController(
theme: AlertControllerTheme(presentationData: presentationData),
title: "Self-Destruct Timer",
text: "Are you sure you want all messages in new chats started by you to be automatically deleted for everyone \(valueText) after they have been sent?",
actions: [
TextAlertAction(type: .defaultAction, title: "Enable Auto-Deletion", action: {
apply(timeout)
}),
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {})
],
actionLayout: .vertical
), nil)
}
}
let arguments = GlobalAutoremoveScreenArguments(
context: context,
updateValue: { value in
updateValue(value)
},
openCustomValue: {
let currentValue = stateValue.with({ $0 }).updatedValue
let controller = ChatTimerScreen(context: context, updatedPresentationData: nil, style: .default, mode: .autoremove, currentTime: currentValue == 0 ? nil : currentValue, dismissByTapOutside: true, completion: { value in
updateValue(value)
})
presentControllerImpl?(controller, nil)
},
infoLinkAction: {
let value = stateValue.with({ $0 }).updatedValue
if value == 0 {
return
}
//TODO:localize
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let valueText = timeIntervalString(strings: presentationData.strings, value: value)
//TODO:localize
let selectionController = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(
context: context,
mode: .chatSelection(ContactMultiselectionControllerMode.ChatSelection(
title: "Select Chats",
searchPlaceholder: "Select chats to apply the \(valueText) self-destruct timer",
selectedChats: Set(),
additionalCategories: nil,
chatListFilters: nil,
displayAutoremoveTimeout: true
)),
options: [],
filters: [.excludeSelf],
isPeerEnabled: { peer in
var canManage = false
if case let .user(user) = peer {
if user.botInfo == nil {
canManage = true
}
} else if case .secretChat = peer {
canManage = true
} else if case let .legacyGroup(group) = peer {
canManage = !group.hasBannedPermission(.banChangeInfo)
} else if case let .channel(channel) = peer {
canManage = channel.hasPermission(.changeInfo)
}
return canManage
}
))
selectionController.navigationPresentation = .modal
let _ = (selectionController.result
|> take(1)
|> deliverOnMainQueue).start(next: { [weak selectionController] result in
var contacts: [ContactListPeerId] = []
if case let .result(peerIdsValue, _) = result {
contacts = peerIdsValue
}
let peerIds = contacts.compactMap { item -> EnginePeer.Id? in
switch item {
case let .peer(id):
return id
case .deviceContact:
return nil
}
}
if peerIds.isEmpty {
selectionController?.dismiss()
} else {
selectionController?.displayProgress = true
let _ = (context.engine.peers.setChatMessageAutoremoveTimeouts(peerIds: peerIds, timeout: value)
|> deliverOnMainQueue).start(completed: {
selectionController?.dismiss()
})
}
})
pushControllerImpl?(selectionController)
}
)
let signal = combineLatest(queue: .mainQueue(),
context.sharedContext.presentationData,
statePromise.get()
)
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let rightNavigationButton: ItemListNavigationButton? = nil
//TODO:localize
let title: ItemListControllerTitle = .text("Auto-Delete Messages")
let entries: [GlobalAutoremoveEntry] = globalAutoremoveScreenEntries(presentationData: presentationData, state: state)
let animateChanges = false
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: title, leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, emptyStateItem: nil, crossfadeState: false, animateChanges: animateChanges, scrollEnabled: true)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(context: context, state: signal)
getController = { [weak controller] in
return controller
}
presentControllerImpl = { [weak controller] c, p in
guard let controller else {
return
}
controller.present(c, in: .window(.root), with: p)
}
pushControllerImpl = { [weak controller] c in
controller?.push(c)
}
dismissImpl = { [weak controller] in
controller?.dismiss()
}
return controller
}

View File

@ -105,7 +105,6 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
case accountHeader(PresentationTheme, String)
case accountTimeout(PresentationTheme, String, String)
case accountInfo(PresentationTheme, String)
case messageAutoremoveHeader(PresentationTheme, String)
case messageAutoremoveTimeout(PresentationTheme, String, String)
case messageAutoremoveInfo(PresentationTheme, String)
case dataSettings(PresentationTheme, String)
@ -113,18 +112,16 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
var section: ItemListSectionId {
switch self {
case .blockedPeers, .activeSessions, .passcode, .twoStepVerification, .loginEmail, .loginEmailInfo:
return PrivacyAndSecuritySection.general.rawValue
case .privacyHeader, .phoneNumberPrivacy, .lastSeenPrivacy, .profilePhotoPrivacy, .forwardPrivacy, .groupPrivacy, .voiceCallPrivacy, .voiceMessagePrivacy, .selectivePrivacyInfo:
return PrivacyAndSecuritySection.privacy.rawValue
case .autoArchiveHeader, .autoArchive, .autoArchiveInfo:
return PrivacyAndSecuritySection.autoArchive.rawValue
case .accountHeader, .accountTimeout, .accountInfo:
return PrivacyAndSecuritySection.account.rawValue
case .messageAutoremoveHeader, .messageAutoremoveTimeout, .messageAutoremoveInfo:
return PrivacyAndSecuritySection.messageAutoremove.rawValue
case .dataSettings, .dataSettingsInfo:
return PrivacyAndSecuritySection.dataSettings.rawValue
case .blockedPeers, .activeSessions, .passcode, .twoStepVerification, .loginEmail, .loginEmailInfo, .messageAutoremoveTimeout, .messageAutoremoveInfo:
return PrivacyAndSecuritySection.general.rawValue
case .privacyHeader, .phoneNumberPrivacy, .lastSeenPrivacy, .profilePhotoPrivacy, .forwardPrivacy, .groupPrivacy, .voiceCallPrivacy, .voiceMessagePrivacy, .selectivePrivacyInfo:
return PrivacyAndSecuritySection.privacy.rawValue
case .autoArchiveHeader, .autoArchive, .autoArchiveInfo:
return PrivacyAndSecuritySection.autoArchive.rawValue
case .accountHeader, .accountTimeout, .accountInfo:
return PrivacyAndSecuritySection.account.rawValue
case .dataSettings, .dataSettingsInfo:
return PrivacyAndSecuritySection.dataSettings.rawValue
}
}
@ -138,50 +135,48 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
return 3
case .twoStepVerification:
return 4
case .loginEmail:
return 5
case .loginEmailInfo:
return 6
case .privacyHeader:
return 7
case .phoneNumberPrivacy:
return 8
case .lastSeenPrivacy:
return 9
case .profilePhotoPrivacy:
return 10
case .voiceCallPrivacy:
return 11
case .voiceMessagePrivacy:
return 12
case .forwardPrivacy:
return 13
case .groupPrivacy:
return 14
case .selectivePrivacyInfo:
return 15
case .autoArchiveHeader:
return 16
case .autoArchive:
return 17
case .autoArchiveInfo:
return 18
case .accountHeader:
return 19
case .accountTimeout:
return 20
case .accountInfo:
return 21
case .messageAutoremoveHeader:
return 22
case .messageAutoremoveTimeout:
return 23
return 5
case .messageAutoremoveInfo:
return 24
return 6
case .loginEmail:
return 7
case .loginEmailInfo:
return 8
case .privacyHeader:
return 9
case .phoneNumberPrivacy:
return 10
case .lastSeenPrivacy:
return 11
case .profilePhotoPrivacy:
return 12
case .voiceCallPrivacy:
return 13
case .voiceMessagePrivacy:
return 14
case .forwardPrivacy:
return 15
case .groupPrivacy:
return 16
case .selectivePrivacyInfo:
return 17
case .autoArchiveHeader:
return 18
case .autoArchive:
return 19
case .autoArchiveInfo:
return 20
case .accountHeader:
return 21
case .accountTimeout:
return 22
case .accountInfo:
return 23
case .dataSettings:
return 25
return 24
case .dataSettingsInfo:
return 26
return 25
}
}
@ -313,12 +308,6 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
} else {
return false
}
case let .messageAutoremoveHeader(lhsTheme, lhsText):
if case let .messageAutoremoveHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .messageAutoremoveTimeout(lhsTheme, lhsText, lhsValue):
if case let .messageAutoremoveTimeout(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
@ -397,6 +386,12 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
return ItemListDisclosureItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/TwoStepAuth")?.precomposed(), title: text, label: value, sectionId: self.section, style: .blocks, action: {
arguments.openTwoStepVerification(data)
})
case let .messageAutoremoveTimeout(_, text, value):
return ItemListDisclosureItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/Timer")?.precomposed(), title: text, label: value, sectionId: self.section, style: .blocks, action: {
arguments.setupMessageAutoremove()
}, tag: PrivacyAndSecurityEntryTag.messageAutoremoveTimeout)
case let .messageAutoremoveInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .loginEmail(_, text, emailPattern):
return ItemListDisclosureItem(presentationData: presentationData, icon: UIImage(bundleImageName: "Settings/Menu/LoginEmail")?.precomposed(), title: text, label: "", sectionId: self.section, style: .blocks, action: {
arguments.openEmailSettings(emailPattern)
@ -423,14 +418,6 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry {
}, tag: PrivacyAndSecurityEntryTag.accountTimeout)
case let .accountInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .messageAutoremoveHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .messageAutoremoveTimeout(_, text, value):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, action: {
arguments.setupMessageAutoremove()
}, tag: PrivacyAndSecurityEntryTag.messageAutoremoveTimeout)
case let .messageAutoremoveInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .dataSettings(_, text):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: {
arguments.openDataSettings()
@ -527,6 +514,26 @@ private func privacyAndSecurityControllerEntries(
}
entries.append(.twoStepVerification(presentationData.theme, presentationData.strings.PrivacySettings_TwoStepAuth, twoStepAuthString, twoStepAuthData))
//TODO:localize
if let privacySettings = privacySettings {
let value: Int32?
if let updatingMessageAutoremoveTimeoutValue = state.updatingMessageAutoremoveTimeoutValue {
value = updatingMessageAutoremoveTimeoutValue
} else {
value = privacySettings.messageAutoremoveTimeout
}
let valueText: String
if let value {
valueText = timeIntervalString(strings: presentationData.strings, value: value)
} else {
valueText = "Off"
}
entries.append(.messageAutoremoveTimeout(presentationData.theme, "Auto-Delete Messages", valueText))
} else {
entries.append(.messageAutoremoveTimeout(presentationData.theme, "Auto-Delete Messages", presentationData.strings.Channel_NotificationLoading))
}
entries.append(.messageAutoremoveInfo(presentationData.theme, "Automatically delete messages for everyone after a period of time in all new chats you start."))
if loginEmail != nil {
entries.append(.loginEmail(presentationData.theme, presentationData.strings.PrivacySettings_LoginEmail, loginEmail))
entries.append(.loginEmailInfo(presentationData.theme, presentationData.strings.PrivacySettings_LoginEmailInfo))
@ -588,27 +595,6 @@ private func privacyAndSecurityControllerEntries(
}
entries.append(.accountInfo(presentationData.theme, presentationData.strings.PrivacySettings_DeleteAccountHelp))
//TODO:localize
entries.append(.messageAutoremoveHeader(presentationData.theme, "MESSAGE TIMER"))
if let privacySettings = privacySettings {
let value: Int32?
if let updatingMessageAutoremoveTimeoutValue = state.updatingMessageAutoremoveTimeoutValue {
value = updatingMessageAutoremoveTimeoutValue
} else {
value = privacySettings.messageAutoremoveTimeout
}
let valueText: String
if let value {
valueText = timeIntervalString(strings: presentationData.strings, value: value)
} else {
valueText = "Never"
}
entries.append(.messageAutoremoveTimeout(presentationData.theme, "Auto-Delete Messages", valueText))
} else {
entries.append(.messageAutoremoveTimeout(presentationData.theme, "Auto-Delete Messages", presentationData.strings.Channel_NotificationLoading))
}
entries.append(.messageAutoremoveInfo(presentationData.theme, "Set a global message timer."))
entries.append(.dataSettings(presentationData.theme, presentationData.strings.PrivacySettings_DataSettings))
entries.append(.dataSettingsInfo(presentationData.theme, presentationData.strings.PrivacySettings_DataSettingsHelp))
@ -1067,79 +1053,21 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting
let signal = privacySettingsPromise.get()
|> take(1)
|> deliverOnMainQueue
updateAccountTimeoutDisposable.set(signal.start(next: { [weak updateAccountTimeoutDisposable] privacySettingsValue in
if let privacySettingsValue = privacySettingsValue {
let timeoutAction: (Int32?) -> Void = { timeout in
if let updateAccountTimeoutDisposable = updateAccountTimeoutDisposable {
updateState { state in
var state = state
state.updatingMessageAutoremoveTimeoutValue = timeout
return state
}
let applyTimeout: Signal<Void, NoError> = privacySettingsPromise.get()
|> filter { $0 != nil }
|> take(1)
|> deliverOnMainQueue
|> mapToSignal { value -> Signal<Void, NoError> in
if let value = value {
privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: timeout)))
}
return .complete()
}
updateAccountTimeoutDisposable.set((context.engine.privacy.updateGlobalMessageRemovalTimeout(timeout: timeout)
|> then(applyTimeout)
|> deliverOnMainQueue).start(completed: {
updateState { state in
var state = state
state.updatingMessageAutoremoveTimeoutValue = nil
return state
}
}))
}
}
let controller = ChatTimerScreen(context: context, updatedPresentationData: nil, style: .default, mode: .autoremove, currentTime: privacySettingsValue.messageAutoremoveTimeout, dismissByTapOutside: true, completion: { value in
timeoutAction(value)
})
presentControllerImpl?(controller)
/*
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = ActionSheetController(presentationData: presentationData)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
let timeoutValues: [Int32] = [
1 * 30 * 24 * 60 * 60,
3 * 30 * 24 * 60 * 60,
6 * 30 * 24 * 60 * 60,
365 * 24 * 60 * 60
]
var timeoutItems: [ActionSheetItem] = timeoutValues.map { value in
return ActionSheetButtonItem(title: timeIntervalString(strings: presentationData.strings, value: value), action: {
dismissAction()
timeoutAction(value)
})
}
timeoutItems.append(ActionSheetButtonItem(title: presentationData.strings.PrivacySettings_DeleteAccountNow, color: .destructive, action: {
dismissAction()
guard let navigationController = getNavigationControllerImpl?() else {
return
}
let _ = (combineLatest(twoStepAuth.get(), twoStepAuthDataValue.get())
updateAccountTimeoutDisposable.set(signal.start(next: { privacySettingsValue in
if let privacySettingsValue {
pushControllerImpl?(globalAutoremoveScreen(context: context, initialValue: privacySettingsValue.messageAutoremoveTimeout ?? 0, updated: { updatedValue in
let applyTimeout: Signal<Void, NoError> = privacySettingsPromise.get()
|> filter { $0 != nil }
|> take(1)
|> deliverOnMainQueue).start(next: { hasTwoStepAuth, twoStepAuthData in
let optionsController = deleteAccountOptionsController(context: context, navigationController: navigationController, hasTwoStepAuth: hasTwoStepAuth ?? false, twoStepAuthData: twoStepAuthData)
pushControllerImpl?(optionsController, true)
})
}))
controller.setItemGroups([
ActionSheetItemGroup(items: timeoutItems),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
presentControllerImpl?(controller)*/
|> deliverOnMainQueue
|> mapToSignal { value -> Signal<Void, NoError> in
if let value = value {
privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, voiceMessages: value.voiceMessages, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout, messageAutoremoveTimeout: updatedValue == 0 ? nil : updatedValue)))
}
return .complete()
}
let _ = applyTimeout.start()
}), true)
}
}))
}, openDataSettings: {

View File

@ -1028,7 +1028,7 @@ public final class ShareController: ViewController {
var peers: [EngineRenderedPeer] = []
for entry in view.0.entries.reversed() {
switch entry {
case let .MessageEntry(_, _, _, _, _, renderedPeer, _, _, _, _, _, _):
case let .MessageEntry(_, _, _, _, _, renderedPeer, _, _, _, _, _, _, _):
if let peer = renderedPeer.peers[renderedPeer.peerId], peer.id != accountPeer.id, canSendMessagesToPeer(peer) {
peers.append(EngineRenderedPeer(renderedPeer))
}

View File

@ -1315,7 +1315,8 @@ private func threadList(context: AccountContext, peerId: EnginePeer.Id) -> Signa
forumTopicData: nil,
topForumTopicItems: [],
hasFailed: false,
isContact: false
isContact: false,
autoremoveTimeout: nil
))
}

View File

@ -596,6 +596,53 @@ public final class MediaStreamComponent: CombinedComponent {
}
strongSelf.hasVideo = true
strongSelf.updated(transition: .immediate)
/*let engine = strongSelf.call.accountContext.engine
guard let info = strongSelf.call.initialCall else {
return
}
let _ = (engine.calls.getAudioBroadcastDataSource(callId: info.id, accessHash: info.accessHash)
|> mapToSignal { source -> Signal<Data?, NoError> in
guard let source else {
return .single(nil)
}
let time = engine.calls.requestStreamState(dataSource: source, callId: info.id, accessHash: info.accessHash)
|> map { state -> Int64? in
guard let state else {
return nil
}
return state.channels.first?.latestTimestamp
}
return time
|> mapToSignal { latestTimestamp -> Signal<Data?, NoError> in
guard let latestTimestamp else {
return .single(nil)
}
let durationMilliseconds: Int64 = 32000
let bufferOffset: Int64 = 1 * durationMilliseconds
let timestampId = latestTimestamp - bufferOffset
return engine.calls.getVideoBroadcastPart(dataSource: source, callId: info.id, accessHash: info.accessHash, timestampIdMilliseconds: timestampId, durationMilliseconds: durationMilliseconds, channelId: 2, quality: 0)
|> mapToSignal { result -> Signal<Data?, NoError> in
switch result.status {
case let .data(data):
return .single(data)
case .notReady, .resyncNeeded, .rejoinNeeded:
return .single(nil)
}
}
}
}
|> deliverOnMainQueue).start(next: { [weak self] data in
guard let self, let data else {
return
}
let _ = self
let _ = data
})*/
})
let callPeer = call.accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: call.peerId))
@ -1116,13 +1163,19 @@ public final class MediaStreamComponent: CombinedComponent {
state.updateDismissOffset(value: offset.y, interactive: true)
case let .ended(velocity):
if abs(velocity.y) > 200.0 {
activatePictureInPicture.invoke(Action { [weak state] in
guard let state = state, let controller = controller() as? MediaStreamComponentController else {
return
if state.isPictureInPictureSupported {
activatePictureInPicture.invoke(Action { [weak state] in
guard let state = state, let controller = controller() as? MediaStreamComponentController else {
return
}
state.updateDismissOffset(value: velocity.y < 0 ? -height : height, interactive: false)
controller.dismiss(closing: false, manual: true)
})
} else {
if let controller = controller() as? MediaStreamComponentController {
controller.dismiss(closing: false, manual: true)
}
state.updateDismissOffset(value: velocity.y < 0 ? -height : height, interactive: false)
controller.dismiss(closing: false, manual: true)
})
}
} else {
state.updateDismissOffset(value: 0.0, interactive: false)
}

View File

@ -609,7 +609,7 @@ public final class PresentationGroupCallImpl: PresentationGroupCall {
private let getDeviceAccessData: () -> (presentationData: PresentationData, present: (ViewController, Any?) -> Void, openSettings: () -> Void)
private var initialCall: EngineGroupCallDescription?
private(set) var initialCall: EngineGroupCallDescription?
public let internalId: CallSessionInternalId
public let peerId: PeerId
private let isChannel: Bool

View File

@ -1091,7 +1091,6 @@ public class Account {
self.managedOperationsDisposable.add(managedSynchronizeConsumeMessageContentOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start())
self.managedOperationsDisposable.add(managedConsumePersonalMessagesActions(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start())
self.managedOperationsDisposable.add(managedReadReactionActions(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start())
self.managedOperationsDisposable.add(managedSynchronizeMessageHistoryTagSummaries(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start())
self.managedOperationsDisposable.add(managedSynchronizeMarkAllUnseenPersonalMessagesOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start())
self.managedOperationsDisposable.add(managedSynchronizeMarkAllUnseenReactionsOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start())
self.managedOperationsDisposable.add(managedApplyPendingMessageReactionsActions(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start())

View File

@ -72,8 +72,8 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))
}))
}
case let .messageActionSetMessagesTTL(period, _, _):
return TelegramMediaAction(action: .messageAutoremoveTimeoutUpdated(period))
case let .messageActionSetMessagesTTL(_, period, autoSettingFrom):
return TelegramMediaAction(action: .messageAutoremoveTimeoutUpdated(period: period, autoSettingSource: autoSettingFrom.flatMap { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) }))
case let .messageActionGroupCallScheduled(call, scheduleDate):
switch call {
case let .inputGroupCall(id, accessHash):

View File

@ -4,19 +4,23 @@ import SwiftSignalKit
func _internal_setSecretChatMessageAutoremoveTimeoutInteractively(account: Account, peerId: PeerId, timeout: Int32?) -> Signal<Void, NoError> {
return account.postbox.transaction { transaction -> Void in
if let peer = transaction.getPeer(peerId) as? TelegramSecretChat, let state = transaction.getPeerChatState(peerId) as? SecretChatState {
if state.messageAutoremoveTimeout != timeout {
let updatedPeer = peer.withUpdatedMessageAutoremoveTimeout(timeout)
let updatedState = state.withUpdatedMessageAutoremoveTimeout(timeout)
if !updatedPeer.isEqual(peer) {
updatePeers(transaction: transaction, peers: [updatedPeer], update: { $1 })
}
if updatedState != state {
transaction.setPeerChatState(peerId, state: updatedState)
}
let _ = enqueueMessages(transaction: transaction, account: account, peerId: peerId, messages: [(true, .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaAction(action: TelegramMediaActionType.messageAutoremoveTimeoutUpdated(timeout == nil ? 0 : timeout!))), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))])
_internal_setSecretChatMessageAutoremoveTimeoutInteractively(transaction: transaction, account: account, peerId: peerId, timeout: timeout)
}
}
func _internal_setSecretChatMessageAutoremoveTimeoutInteractively(transaction: Transaction, account: Account, peerId: PeerId, timeout: Int32?) {
if let peer = transaction.getPeer(peerId) as? TelegramSecretChat, let state = transaction.getPeerChatState(peerId) as? SecretChatState {
if state.messageAutoremoveTimeout != timeout {
let updatedPeer = peer.withUpdatedMessageAutoremoveTimeout(timeout)
let updatedState = state.withUpdatedMessageAutoremoveTimeout(timeout)
if !updatedPeer.isEqual(peer) {
updatePeers(transaction: transaction, peers: [updatedPeer], update: { $1 })
}
if updatedState != state {
transaction.setPeerChatState(peerId, state: updatedState)
}
let _ = enqueueMessages(transaction: transaction, account: account, peerId: peerId, messages: [(true, .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaAction(action: TelegramMediaActionType.messageAutoremoveTimeoutUpdated(period: timeout == nil ? 0 : timeout!, autoSettingSource: nil))), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))])
}
}
}

View File

@ -2211,7 +2211,7 @@ private func resolveMissingPeerChatInfos(network: Network, state: AccountMutable
for dialog in dialogs {
switch dialog {
case let .dialog(_, peer, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, unreadMentionsCount, unreadReactionsCount, notifySettings, pts, _, folderId, _):
case let .dialog(_, peer, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, unreadMentionsCount, unreadReactionsCount, notifySettings, pts, _, folderId, ttlPeriod):
let peerId = peer.peerId
updatedState.setNeedsHoleFromPreviousState(peerId: peerId, namespace: Namespaces.Message.Cloud, validateChannelPts: pts)
@ -2251,6 +2251,8 @@ private func resolveMissingPeerChatInfos(network: Network, state: AccountMutable
if !isExcludedFromChatList {
updatedState.updatePeerChatInclusion(peerId: peerId, groupId: PeerGroupId(rawValue: folderId ?? 0), changedGroup: false)
}
updatedState.updateAutoremoveTimeout(peer: peer, value: ttlPeriod.flatMap(CachedPeerAutoremoveTimeout.Value.init(peerValue:)))
let notificationSettings = TelegramPeerNotificationSettings(apiSettings: notifySettings)
updatedState.updateNotificationSettings(.peer(peerId: peer.peerId, threadId: nil), notificationSettings: notificationSettings)
@ -2452,8 +2454,9 @@ private func resetChannels(accountPeerId: PeerId, postbox: Postbox, network: Net
let apiNotificationSettings: Api.PeerNotifySettings
let apiMarkedUnread: Bool
let groupId: PeerGroupId
let apiTtlPeriod: Int32?
switch dialog {
case let .dialog(flags, peer, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, unreadMentionsCount, unreadReactionsCount, peerNotificationSettings, pts, _, folderId, _):
case let .dialog(flags, peer, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, unreadMentionsCount, unreadReactionsCount, peerNotificationSettings, pts, _, folderId, ttlPeriod):
apiPeer = peer
apiTopMessage = topMessage
apiReadInboxMaxId = readInboxMaxId
@ -2465,6 +2468,7 @@ private func resetChannels(accountPeerId: PeerId, postbox: Postbox, network: Net
apiNotificationSettings = peerNotificationSettings
apiChannelPts = pts
groupId = PeerGroupId(rawValue: folderId ?? 0)
apiTtlPeriod = ttlPeriod
case .dialogFolder:
assertionFailure()
continue loop
@ -2491,6 +2495,8 @@ private func resetChannels(accountPeerId: PeerId, postbox: Postbox, network: Net
updatedState.updatePeerChatInclusion(peerId: peerId, groupId: groupId, changedGroup: false)
updatedState.updateAutoremoveTimeout(peer: apiPeer, value: apiTtlPeriod.flatMap(CachedPeerAutoremoveTimeout.Value.init(peerValue:)))
resetForumTopics.insert(peerId)
}
@ -2747,12 +2753,12 @@ private func pollChannel(accountPeerId: PeerId, postbox: Postbox, network: Netwo
apiTimeout = timeout
var parameters: (peer: Api.Peer, pts: Int32, topMessage: Int32, readInboxMaxId: Int32, readOutboxMaxId: Int32, unreadCount: Int32, unreadMentionsCount: Int32, unreadReactionsCount: Int32)?
var parameters: (peer: Api.Peer, pts: Int32, topMessage: Int32, readInboxMaxId: Int32, readOutboxMaxId: Int32, unreadCount: Int32, unreadMentionsCount: Int32, unreadReactionsCount: Int32, ttlPeriod: Int32?)?
switch dialog {
case let .dialog(_, peer, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, unreadMentionsCount, unreadReactionsCount, _, pts, _, _, _):
case let .dialog(_, peer, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, unreadMentionsCount, unreadReactionsCount, _, pts, _, _, ttlPeriod):
if let pts = pts {
parameters = (peer, pts, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, unreadMentionsCount, unreadReactionsCount)
parameters = (peer, pts, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, unreadMentionsCount, unreadReactionsCount, ttlPeriod)
}
case .dialogFolder:
break
@ -2760,10 +2766,12 @@ private func pollChannel(accountPeerId: PeerId, postbox: Postbox, network: Netwo
var resetForumTopics = Set<PeerId>()
if let (peer, pts, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, unreadMentionsCount, unreadReactionsCount) = parameters {
if let (peer, pts, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, unreadMentionsCount, unreadReactionsCount, ttlPeriod) = parameters {
updatedState.updateChannelState(peer.peerId, pts: pts)
updatedState.updateChannelInvalidationPts(peer.peerId, invalidationPts: pts)
updatedState.updateAutoremoveTimeout(peer: peer, value: ttlPeriod.flatMap(CachedPeerAutoremoveTimeout.Value.init(peerValue:)))
updatedState.mergeChats(chats)
updatedState.mergeUsers(users)

View File

@ -22,6 +22,7 @@ struct ParsedDialogs {
let channelStates: [PeerId: Int32]
let topMessageIds: [PeerId: MessageId]
let storeMessages: [StoreMessage]
let ttlPeriods: [PeerId: CachedPeerAutoremoveTimeout]
let lowerNonPinnedIndex: MessageIndex?
let referencedFolders: [PeerGroupId: PeerGroupUnreadCountersSummary]
@ -53,6 +54,7 @@ private func parseDialogs(apiDialogs: [Api.Dialog], apiMessages: [Api.Message],
var reactionTagSummaries: [PeerId: MessageHistoryTagNamespaceSummary] = [:]
var channelStates: [PeerId: Int32] = [:]
var topMessageIds: [PeerId: MessageId] = [:]
var ttlPeriods: [PeerId: CachedPeerAutoremoveTimeout] = [:]
var storeMessages: [StoreMessage] = []
var nonPinnedDialogsTopMessageIds = Set<MessageId>()
@ -85,7 +87,7 @@ private func parseDialogs(apiDialogs: [Api.Dialog], apiMessages: [Api.Message],
var apiChannelPts: Int32?
let apiNotificationSettings: Api.PeerNotifySettings
switch dialog {
case let .dialog(flags, peer, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, unreadMentionsCount, unreadReactionsCount, peerNotificationSettings, pts, _, _, _):
case let .dialog(flags, peer, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, unreadMentionsCount, unreadReactionsCount, peerNotificationSettings, pts, _, _, ttlPeriod):
if let peer = peers[peer.peerId] {
var isExluded = false
if let group = peer as? TelegramGroup {
@ -107,6 +109,9 @@ private func parseDialogs(apiDialogs: [Api.Dialog], apiMessages: [Api.Message],
apiUnreadReactionsCount = unreadReactionsCount
apiNotificationSettings = peerNotificationSettings
apiChannelPts = pts
ttlPeriods[peer.peerId] = .known(ttlPeriod.flatMap(CachedPeerAutoremoveTimeout.Value.init(peerValue:)))
let isPinned = (flags & (1 << 2)) != 0
if !isPinned {
nonPinnedDialogsTopMessageIds.insert(MessageId(peerId: peer.peerId, namespace: Namespaces.Message.Cloud, id: topMessage))
@ -180,6 +185,7 @@ private func parseDialogs(apiDialogs: [Api.Dialog], apiMessages: [Api.Message],
channelStates: channelStates,
topMessageIds: topMessageIds,
storeMessages: storeMessages,
ttlPeriods: ttlPeriods,
lowerNonPinnedIndex: lowerNonPinnedIndex,
referencedFolders: referencedFolders
@ -191,6 +197,7 @@ struct FetchedChatList {
var peers: [Peer]
var peerPresences: [PeerId: Api.User]
var notificationSettings: [PeerId: PeerNotificationSettings]
var ttlPeriods: [PeerId: CachedPeerAutoremoveTimeout]
var readStates: [PeerId: [MessageId.Namespace: PeerReadState]]
var mentionTagSummaries: [PeerId: MessageHistoryTagNamespaceSummary]
var reactionTagSummaries: [PeerId: MessageHistoryTagNamespaceSummary]
@ -298,6 +305,7 @@ func fetchChatList(postbox: Postbox, network: Network, location: FetchChatListLo
var peers: [Peer] = []
var peerPresences: [PeerId: Api.User] = [:]
var notificationSettings: [PeerId: PeerNotificationSettings] = [:]
var ttlPeriods: [PeerId: CachedPeerAutoremoveTimeout] = [:]
var readStates: [PeerId: [MessageId.Namespace: PeerReadState]] = [:]
var mentionTagSummaries: [PeerId: MessageHistoryTagNamespaceSummary] = [:]
var reactionTagSummaries: [PeerId: MessageHistoryTagNamespaceSummary] = [:]
@ -308,6 +316,7 @@ func fetchChatList(postbox: Postbox, network: Network, location: FetchChatListLo
peers.append(contentsOf: parsedRemoteChats.peers)
peerPresences.merge(parsedRemoteChats.peerPresences, uniquingKeysWith: { _, updated in updated })
notificationSettings.merge(parsedRemoteChats.notificationSettings, uniquingKeysWith: { _, updated in updated })
ttlPeriods.merge(parsedRemoteChats.ttlPeriods, uniquingKeysWith: { _, updated in updated })
readStates.merge(parsedRemoteChats.readStates, uniquingKeysWith: { _, updated in updated })
mentionTagSummaries.merge(parsedRemoteChats.mentionTagSummaries, uniquingKeysWith: { _, updated in updated })
reactionTagSummaries.merge(parsedRemoteChats.reactionTagSummaries, uniquingKeysWith: { _, updated in updated })
@ -319,6 +328,7 @@ func fetchChatList(postbox: Postbox, network: Network, location: FetchChatListLo
peers.append(contentsOf: parsedPinnedChats.peers)
peerPresences.merge(parsedPinnedChats.peerPresences, uniquingKeysWith: { _, updated in updated })
notificationSettings.merge(parsedPinnedChats.notificationSettings, uniquingKeysWith: { _, updated in updated })
ttlPeriods.merge(parsedPinnedChats.ttlPeriods, uniquingKeysWith: { _, updated in updated })
readStates.merge(parsedPinnedChats.readStates, uniquingKeysWith: { _, updated in updated })
mentionTagSummaries.merge(parsedPinnedChats.mentionTagSummaries, uniquingKeysWith: { _, updated in updated })
reactionTagSummaries.merge(parsedPinnedChats.reactionTagSummaries, uniquingKeysWith: { _, updated in updated })
@ -342,6 +352,7 @@ func fetchChatList(postbox: Postbox, network: Network, location: FetchChatListLo
peers.append(contentsOf: folderChats.peers)
peerPresences.merge(folderChats.peerPresences, uniquingKeysWith: { _, updated in updated })
notificationSettings.merge(folderChats.notificationSettings, uniquingKeysWith: { _, updated in updated })
ttlPeriods.merge(folderChats.ttlPeriods, uniquingKeysWith: { _, updated in updated })
readStates.merge(folderChats.readStates, uniquingKeysWith: { _, updated in updated })
mentionTagSummaries.merge(folderChats.mentionTagSummaries, uniquingKeysWith: { _, updated in updated })
reactionTagSummaries.merge(folderChats.reactionTagSummaries, uniquingKeysWith: { _, updated in updated })
@ -376,6 +387,7 @@ func fetchChatList(postbox: Postbox, network: Network, location: FetchChatListLo
peers: peers,
peerPresences: peerPresences,
notificationSettings: notificationSettings,
ttlPeriods: ttlPeriods,
readStates: readStates,
mentionTagSummaries: mentionTagSummaries,
reactionTagSummaries: reactionTagSummaries,

View File

@ -815,6 +815,20 @@ func fetchChatListHole(postbox: Postbox, network: Network, accountPeerId: PeerId
let _ = transaction.addMessages(additionalMessages, location: .Random)
transaction.resetIncomingReadStates(fetchedChats.readStates)
for (peerId, autoremoveValue) in fetchedChats.ttlPeriods {
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
if let current = current as? CachedUserData {
return current.withUpdatedAutoremoveTimeout(autoremoveValue)
} else if let current = current as? CachedGroupData {
return current.withUpdatedAutoremoveTimeout(autoremoveValue)
} else if let current = current as? CachedChannelData {
return current.withUpdatedAutoremoveTimeout(autoremoveValue)
} else {
return current
}
})
}
transaction.replaceChatListHole(groupId: groupId, index: hole.index, hole: fetchedChats.lowerNonPinnedIndex.flatMap(ChatListHole.init))
for peerId in fetchedChats.chatPeerIds {

View File

@ -93,7 +93,7 @@ func managedConsumePersonalMessagesActions(postbox: Postbox, network: Network, s
let helper = Atomic<ManagedConsumePersonalMessagesActionsHelper>(value: ManagedConsumePersonalMessagesActionsHelper())
let actionsKey = PostboxViewKey.pendingMessageActions(type: .consumeUnseenPersonalMessage)
let invalidateKey = PostboxViewKey.invalidatedMessageHistoryTagSummaries(tagMask: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud)
let invalidateKey = PostboxViewKey.invalidatedMessageHistoryTagSummaries(peerId: nil, threadId: nil, tagMask: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud)
let disposable = postbox.combinedView(keys: [actionsKey, invalidateKey]).start(next: { view in
var entries: [PendingMessageActionsEntry] = []
var invalidateEntries = Set<InvalidatedMessageHistoryTagsSummaryEntry>()
@ -156,7 +156,7 @@ func managedReadReactionActions(postbox: Postbox, network: Network, stateManager
let helper = Atomic<ManagedConsumePersonalMessagesActionsHelper>(value: ManagedConsumePersonalMessagesActionsHelper())
let actionsKey = PostboxViewKey.pendingMessageActions(type: .readReaction)
let invalidateKey = PostboxViewKey.invalidatedMessageHistoryTagSummaries(tagMask: .unseenReaction, namespace: Namespaces.Message.Cloud)
let invalidateKey = PostboxViewKey.invalidatedMessageHistoryTagSummaries(peerId: nil, threadId: nil, tagMask: .unseenReaction, namespace: Namespaces.Message.Cloud)
let disposable = postbox.combinedView(keys: [actionsKey, invalidateKey]).start(next: { view in
var entries: [PendingMessageActionsEntry] = []
var invalidateEntries = Set<InvalidatedMessageHistoryTagsSummaryEntry>()
@ -435,11 +435,11 @@ private func synchronizeUnseenReactionsTag(postbox: Postbox, network: Network, e
} |> switchToLatest
}
func managedSynchronizeMessageHistoryTagSummaries(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal<Void, NoError> {
func managedSynchronizeMessageHistoryTagSummaries(postbox: Postbox, network: Network, stateManager: AccountStateManager, peerId: PeerId, threadId: Int64) -> Signal<Void, NoError> {
return Signal { _ in
let helper = Atomic<ManagedConsumePersonalMessagesActionsHelper>(value: ManagedConsumePersonalMessagesActionsHelper())
let invalidateKey = PostboxViewKey.invalidatedMessageHistoryTagSummaries(tagMask: MessageTags(rawValue: 0), namespace: Namespaces.Message.Cloud)
let invalidateKey = PostboxViewKey.invalidatedMessageHistoryTagSummaries(peerId: peerId, threadId: threadId, tagMask: MessageTags(rawValue: 0), namespace: Namespaces.Message.Cloud)
let disposable = postbox.combinedView(keys: [invalidateKey]).start(next: { view in
var invalidateEntries = Set<InvalidatedMessageHistoryTagsSummaryEntry>()
if let v = view.views[invalidateKey] as? InvalidatedMessageHistoryTagSummariesView {
@ -480,7 +480,7 @@ private func synchronizeMessageHistoryTagSummary(postbox: Postbox, network: Netw
guard let threadId = entry.key.threadId else {
return .complete()
}
if let peer = transaction.getPeer(entry.key.peerId), let inputPeer = apiInputPeer(peer) {
if let peer = transaction.getPeer(entry.key.peerId) as? TelegramChannel, peer.flags.contains(.isForum), let inputPeer = apiInputPeer(peer) {
return network.request(Api.functions.messages.getReplies(peer: inputPeer, msgId: Int32(clamping: threadId), offsetId: 0, offsetDate: 0, addOffset: 0, limit: 1, maxId: 0, minId: 0, hash: 0))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.Messages?, NoError> in

View File

@ -132,6 +132,7 @@ private func synchronizePinnedChats(transaction: Transaction, postbox: Postbox,
var readStates: [PeerId: [MessageId.Namespace: PeerReadState]] = [:]
var channelStates: [PeerId: Int32] = [:]
var notificationSettings: [PeerId: PeerNotificationSettings] = [:]
var ttlPeriods: [PeerId: CachedPeerAutoremoveTimeout] = [:]
var remoteItemIds: [PinnedItemId] = []
@ -159,9 +160,10 @@ private func synchronizePinnedChats(transaction: Transaction, postbox: Postbox,
let apiUnreadCount: Int32
let apiMarkedUnread: Bool
var apiChannelPts: Int32?
let apiTtlPeriod: Int32?
let apiNotificationSettings: Api.PeerNotifySettings
switch dialog {
case let .dialog(flags, peer, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, _, _, peerNotificationSettings, pts, _, _, _):
case let .dialog(flags, peer, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, _, _, peerNotificationSettings, pts, _, _, ttlPeriod):
apiPeer = peer
apiTopMessage = topMessage
apiReadInboxMaxId = readInboxMaxId
@ -170,6 +172,7 @@ private func synchronizePinnedChats(transaction: Transaction, postbox: Postbox,
apiMarkedUnread = (flags & (1 << 3)) != 0
apiNotificationSettings = peerNotificationSettings
apiChannelPts = pts
apiTtlPeriod = ttlPeriod
case .dialogFolder:
//assertionFailure()
continue loop
@ -189,6 +192,8 @@ private func synchronizePinnedChats(transaction: Transaction, postbox: Postbox,
}
notificationSettings[peerId] = TelegramPeerNotificationSettings(apiSettings: apiNotificationSettings)
ttlPeriods[peerId] = .known(apiTtlPeriod.flatMap(CachedPeerAutoremoveTimeout.Value.init(peerValue:)))
}
for message in messages {
@ -220,6 +225,20 @@ private func synchronizePinnedChats(transaction: Transaction, postbox: Postbox,
transaction.updateCurrentPeerNotificationSettings(notificationSettings)
for (peerId, autoremoveValue) in ttlPeriods {
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
if let current = current as? CachedUserData {
return current.withUpdatedAutoremoveTimeout(autoremoveValue)
} else if let current = current as? CachedGroupData {
return current.withUpdatedAutoremoveTimeout(autoremoveValue)
} else if let current = current as? CachedChannelData {
return current.withUpdatedAutoremoveTimeout(autoremoveValue)
} else {
return current
}
})
}
var allPeersWithMessages = Set<PeerId>()
for message in storeMessages {
if !allPeersWithMessages.contains(message.id.peerId) {

View File

@ -950,7 +950,7 @@ public final class PendingMessageManager {
var sentAsAction = false
for media in message.media {
if let media = media as? TelegramMediaAction {
if case let .messageAutoremoveTimeoutUpdated(value) = media.action {
if case let .messageAutoremoveTimeoutUpdated(value, _) = media.action {
sentAsAction = true
let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: message.id.peerId, operation: .setMessageAutoremoveTimeout(layer: layer, actionGloballyUniqueId: message.globallyUniqueId!, timeout: value, messageId: message.id), state: state)
if updatedState != state {

View File

@ -581,7 +581,7 @@ extension StoreMessage {
case .decryptedMessageActionScreenshotMessages:
self.init(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: tagLocalIndex), globallyUniqueId: randomId, groupingKey: nil, threadId: nil, timestamp: timestamp, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: authorId, text: "", attributes: [], media: [TelegramMediaAction(action: .historyScreenshot)])
case let .decryptedMessageActionSetMessageTTL(ttlSeconds):
self.init(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: tagLocalIndex), globallyUniqueId: randomId, groupingKey: nil, threadId: nil, timestamp: timestamp, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: authorId, text: "", attributes: [], media: [TelegramMediaAction(action: .messageAutoremoveTimeoutUpdated(ttlSeconds))])
self.init(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: tagLocalIndex), globallyUniqueId: randomId, groupingKey: nil, threadId: nil, timestamp: timestamp, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: authorId, text: "", attributes: [], media: [TelegramMediaAction(action: .messageAutoremoveTimeoutUpdated(period: ttlSeconds, autoSettingSource: nil))])
}
}
}
@ -908,7 +908,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32
case .decryptedMessageActionScreenshotMessages:
return (StoreMessage(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: tagLocalIndex), globallyUniqueId: randomId, groupingKey: nil, threadId: nil, timestamp: timestamp, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: authorId, text: "", attributes: [], media: [TelegramMediaAction(action: .historyScreenshot)]), [])
case let .decryptedMessageActionSetMessageTTL(ttlSeconds):
return (StoreMessage(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: tagLocalIndex), globallyUniqueId: randomId, groupingKey: nil, threadId: nil, timestamp: timestamp, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: authorId, text: "", attributes: [], media: [TelegramMediaAction(action: .messageAutoremoveTimeoutUpdated(ttlSeconds))]), [])
return (StoreMessage(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: tagLocalIndex), globallyUniqueId: randomId, groupingKey: nil, threadId: nil, timestamp: timestamp, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: authorId, text: "", attributes: [], media: [TelegramMediaAction(action: .messageAutoremoveTimeoutUpdated(period: ttlSeconds, autoSettingSource: nil))]), [])
case .decryptedMessageActionResend:
return nil
case .decryptedMessageActionRequestKey:
@ -1140,7 +1140,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32
case .decryptedMessageActionScreenshotMessages:
return (StoreMessage(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: tagLocalIndex), globallyUniqueId: randomId, groupingKey: nil, threadId: nil, timestamp: timestamp, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: authorId, text: "", attributes: [], media: [TelegramMediaAction(action: .historyScreenshot)]), [])
case let .decryptedMessageActionSetMessageTTL(ttlSeconds):
return (StoreMessage(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: tagLocalIndex), globallyUniqueId: randomId, groupingKey: nil, threadId: nil, timestamp: timestamp, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: authorId, text: "", attributes: [], media: [TelegramMediaAction(action: .messageAutoremoveTimeoutUpdated(ttlSeconds))]), [])
return (StoreMessage(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: tagLocalIndex), globallyUniqueId: randomId, groupingKey: nil, threadId: nil, timestamp: timestamp, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: authorId, text: "", attributes: [], media: [TelegramMediaAction(action: .messageAutoremoveTimeoutUpdated(period: ttlSeconds, autoSettingSource: nil))]), [])
case .decryptedMessageActionResend:
return nil
case .decryptedMessageActionRequestKey:
@ -1419,7 +1419,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32
case .decryptedMessageActionScreenshotMessages:
return (StoreMessage(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: tagLocalIndex), globallyUniqueId: randomId, groupingKey: nil, threadId: nil, timestamp: timestamp, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: authorId, text: "", attributes: [], media: [TelegramMediaAction(action: .historyScreenshot)]), [])
case let .decryptedMessageActionSetMessageTTL(ttlSeconds):
return (StoreMessage(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: tagLocalIndex), globallyUniqueId: randomId, groupingKey: nil, threadId: nil, timestamp: timestamp, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: authorId, text: "", attributes: [], media: [TelegramMediaAction(action: .messageAutoremoveTimeoutUpdated(ttlSeconds))]), [])
return (StoreMessage(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: tagLocalIndex), globallyUniqueId: randomId, groupingKey: nil, threadId: nil, timestamp: timestamp, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: authorId, text: "", attributes: [], media: [TelegramMediaAction(action: .messageAutoremoveTimeoutUpdated(period: ttlSeconds, autoSettingSource: nil))]), [])
case .decryptedMessageActionResend:
return nil
case .decryptedMessageActionRequestKey:
@ -1620,7 +1620,7 @@ private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32
case .decryptedMessageActionScreenshotMessages:
return (StoreMessage(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: tagLocalIndex), globallyUniqueId: randomId, groupingKey: nil, threadId: nil, timestamp: timestamp, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: authorId, text: "", attributes: [], media: [TelegramMediaAction(action: .historyScreenshot)]), [])
case let .decryptedMessageActionSetMessageTTL(ttlSeconds):
return (StoreMessage(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: tagLocalIndex), globallyUniqueId: randomId, groupingKey: nil, threadId: nil, timestamp: timestamp, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: authorId, text: "", attributes: [], media: [TelegramMediaAction(action: .messageAutoremoveTimeoutUpdated(ttlSeconds))]), [])
return (StoreMessage(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: tagLocalIndex), globallyUniqueId: randomId, groupingKey: nil, threadId: nil, timestamp: timestamp, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: authorId, text: "", attributes: [], media: [TelegramMediaAction(action: .messageAutoremoveTimeoutUpdated(period: ttlSeconds, autoSettingSource: nil))]), [])
case .decryptedMessageActionResend:
return nil
case .decryptedMessageActionRequestKey:

View File

@ -133,6 +133,22 @@ public let telegramPostboxSeedConfiguration: SeedConfiguration = {
return nil
}
return Message.AssociatedThreadInfo(title: data.info.title, icon: data.info.icon, iconColor: data.info.iconColor)
},
decodeAutoremoveTimeout: { cachedData in
if let cachedData = cachedData as? CachedUserData {
if case let .known(value) = cachedData.autoremoveTimeout {
return value?.effectiveValue
}
} else if let cachedData = cachedData as? CachedGroupData {
if case let .known(value) = cachedData.autoremoveTimeout {
return value?.effectiveValue
}
} else if let cachedData = cachedData as? CachedChannelData {
if case let .known(value) = cachedData.autoremoveTimeout {
return value?.effectiveValue
}
}
return nil
}
)
}()

View File

@ -80,7 +80,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case groupMigratedToChannel(channelId: PeerId)
case historyCleared
case historyScreenshot
case messageAutoremoveTimeoutUpdated(Int32)
case messageAutoremoveTimeoutUpdated(period: Int32, autoSettingSource: PeerId?)
case gameScore(gameId: Int64, score: Int32)
case phoneCall(callId: Int64, discardReason: PhoneCallDiscardReason?, duration: Int32?, isVideo: Bool)
case paymentSent(currency: String, totalAmount: Int64, invoiceSlug: String?, isRecurringInit: Bool, isRecurringUsed: Bool)
@ -125,7 +125,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case 11:
self = .historyScreenshot
case 12:
self = .messageAutoremoveTimeoutUpdated(decoder.decodeInt32ForKey("t", orElse: 0))
self = .messageAutoremoveTimeoutUpdated(period: decoder.decodeInt32ForKey("t", orElse: 0), autoSettingSource: decoder.decodeOptionalInt64ForKey("src").flatMap(PeerId.init))
case 13:
self = .gameScore(gameId: decoder.decodeInt64ForKey("i", orElse: 0), score: decoder.decodeInt32ForKey("s", orElse: 0))
case 14:
@ -218,9 +218,14 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
encoder.encodeInt32(10, forKey: "_rawValue")
case .historyScreenshot:
encoder.encodeInt32(11, forKey: "_rawValue")
case let .messageAutoremoveTimeoutUpdated(timeout):
case let .messageAutoremoveTimeoutUpdated(timeout, autoSettingSource):
encoder.encodeInt32(12, forKey: "_rawValue")
encoder.encodeInt32(timeout, forKey: "t")
if let autoSettingSource = autoSettingSource {
encoder.encodeInt64(autoSettingSource.toInt64(), forKey: "src")
} else {
encoder.encodeNil(forKey: "src")
}
case let .gameScore(gameId, score):
encoder.encodeInt32(13, forKey: "_rawValue")
encoder.encodeInt64(gameId, forKey: "i")

View File

@ -2532,6 +2532,8 @@ func _internal_getVideoBroadcastPart(dataSource: AudioBroadcastDataSource, callI
scale = 0
case 500:
scale = 1
case 32000:
scale = -5
default:
return .single(GetAudioBroadcastPartResult(status: .notReady, responseTimestamp: Double(timestampIdMilliseconds) / 1000.0))
}

View File

@ -126,6 +126,7 @@ public final class EngineChatList: Equatable {
public let topForumTopicItems: [EngineChatList.ForumTopicData]
public let hasFailed: Bool
public let isContact: Bool
public let autoremoveTimeout: Int32?
public init(
id: Id,
@ -142,7 +143,8 @@ public final class EngineChatList: Equatable {
forumTopicData: ForumTopicData?,
topForumTopicItems: [EngineChatList.ForumTopicData],
hasFailed: Bool,
isContact: Bool
isContact: Bool,
autoremoveTimeout: Int32?
) {
self.id = id
self.index = index
@ -159,6 +161,7 @@ public final class EngineChatList: Equatable {
self.topForumTopicItems = topForumTopicItems
self.hasFailed = hasFailed
self.isContact = isContact
self.autoremoveTimeout = autoremoveTimeout
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
@ -207,6 +210,9 @@ public final class EngineChatList: Equatable {
if lhs.isContact != rhs.isContact {
return false
}
if lhs.autoremoveTimeout != rhs.autoremoveTimeout {
return false
}
return true
}
}
@ -408,7 +414,7 @@ public extension EngineChatList.RelativePosition {
extension EngineChatList.Item {
convenience init?(_ entry: ChatListEntry) {
switch entry {
case let .MessageEntry(index, messages, readState, isRemovedFromTotalUnreadCount, embeddedState, renderedPeer, presence, tagSummaryInfo, forumTopicData, topForumTopics, hasFailed, isContact):
case let .MessageEntry(index, messages, readState, isRemovedFromTotalUnreadCount, embeddedState, renderedPeer, presence, tagSummaryInfo, forumTopicData, topForumTopics, hasFailed, isContact, autoremoveTimeout):
var draft: EngineChatList.Draft?
if let embeddedState = embeddedState, let _ = embeddedState.overrideChatTimestamp {
if let opaqueState = _internal_decodeStoredChatInterfaceState(state: embeddedState) {
@ -470,7 +476,8 @@ extension EngineChatList.Item {
forumTopicData: forumTopicDataValue,
topForumTopicItems: topForumTopicItems,
hasFailed: hasFailed,
isContact: isContact
isContact: isContact,
autoremoveTimeout: autoremoveTimeout
)
case .HoleEntry:
return nil

View File

@ -590,6 +590,14 @@ public struct ChatReplyThreadMessage: Equatable {
self.initialAnchor = initialAnchor
self.isNotAvailable = isNotAvailable
}
public var normalized: ChatReplyThreadMessage {
if self.isForumPost {
return ChatReplyThreadMessage(messageId: self.messageId, channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false)
} else {
return self
}
}
}
public enum FetchChannelReplyThreadMessageError {

View File

@ -494,5 +494,10 @@ public extension TelegramEngine {
}
|> ignoreValues
}
public func keepMessageCountersSyncrhonized(peerId: EnginePeer.Id, threadId: Int64) -> Signal<Never, NoError> {
return managedSynchronizeMessageHistoryTagSummaries(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, peerId: peerId, threadId: threadId)
|> ignoreValues
}
}
}

View File

@ -635,6 +635,7 @@ private func loadAndStorePeerChatInfos(accountPeerId: PeerId, postbox: Postbox,
var peers: [Peer] = []
var peerPresences: [PeerId: Api.User] = [:]
var notificationSettings: [PeerId: PeerNotificationSettings] = [:]
var ttlPeriods: [PeerId: CachedPeerAutoremoveTimeout] = [:]
var channelStates: [PeerId: Int32] = [:]
switch result {
@ -654,7 +655,7 @@ private func loadAndStorePeerChatInfos(accountPeerId: PeerId, postbox: Postbox,
for dialog in dialogs {
switch dialog {
case let .dialog(_, peer, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, unreadMentionsCount, unreadReactionsCount, notifySettings, pts, _, folderId, _):
case let .dialog(_, peer, topMessage, readInboxMaxId, readOutboxMaxId, unreadCount, unreadMentionsCount, unreadReactionsCount, notifySettings, pts, _, folderId, ttlPeriod):
let peerId = peer.peerId
if topMessage != 0 {
@ -708,6 +709,8 @@ private func loadAndStorePeerChatInfos(accountPeerId: PeerId, postbox: Postbox,
notificationSettings[peer.peerId] = TelegramPeerNotificationSettings(apiSettings: notifySettings)
ttlPeriods[peer.peerId] = .known(ttlPeriod.flatMap(CachedPeerAutoremoveTimeout.Value.init(peerValue:)))
transaction.resetIncomingReadStates([peerId: [Namespaces.Message.Cloud: .idBased(maxIncomingReadId: readInboxMaxId, maxOutgoingReadId: readOutboxMaxId, maxKnownId: topMessage, count: unreadCount, markedUnread: false)]])
transaction.replaceMessageTagSummary(peerId: peerId, threadId: nil, tagMask: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud, count: unreadMentionsCount, maxId: topMessage)
@ -754,6 +757,20 @@ private func loadAndStorePeerChatInfos(accountPeerId: PeerId, postbox: Postbox,
updatePeerPresences(transaction: transaction, accountPeerId: accountPeerId, peerPresences: peerPresences)
transaction.updateCurrentPeerNotificationSettings(notificationSettings)
for (peerId, autoremoveValue) in ttlPeriods {
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
if let current = current as? CachedUserData {
return current.withUpdatedAutoremoveTimeout(autoremoveValue)
} else if let current = current as? CachedGroupData {
return current.withUpdatedAutoremoveTimeout(autoremoveValue)
} else if let current = current as? CachedChannelData {
return current.withUpdatedAutoremoveTimeout(autoremoveValue)
} else {
return current
}
})
}
}
|> ignoreValues
}

View File

@ -154,6 +154,56 @@ public extension TelegramEngine {
return _internal_setChatMessageAutoremoveTimeoutInteractively(account: self.account, peerId: peerId, timeout: timeout)
}
}
public func setChatMessageAutoremoveTimeouts(peerIds: [EnginePeer.Id], timeout: Int32?) -> Signal<Never, NoError> {
return self.account.postbox.transaction { transaction -> Void in
for peerId in peerIds {
if peerId.namespace == Namespaces.Peer.SecretChat {
_internal_setSecretChatMessageAutoremoveTimeoutInteractively(transaction: transaction, account: self.account, peerId: peerId, timeout: timeout)
} else {
var canManage = false
guard let peer = transaction.getPeer(peerId) else {
continue
}
if let user = peer as? TelegramUser {
if user.botInfo == nil {
canManage = true
}
} else if let _ = peer as? TelegramSecretChat {
canManage = true
} else if let group = peer as? TelegramGroup {
canManage = !group.hasBannedPermission(.banChangeInfo)
} else if let channel = peer as? TelegramChannel {
canManage = channel.hasPermission(.changeInfo)
}
if !canManage {
continue
}
let cachedData = transaction.getPeerCachedData(peerId: peerId)
var currentValue: Int32?
if let cachedData = cachedData as? CachedUserData {
if case let .known(value) = cachedData.autoremoveTimeout {
currentValue = value?.effectiveValue
}
} else if let cachedData = cachedData as? CachedGroupData {
if case let .known(value) = cachedData.autoremoveTimeout {
currentValue = value?.effectiveValue
}
} else if let cachedData = cachedData as? CachedChannelData {
if case let .known(value) = cachedData.autoremoveTimeout {
currentValue = value?.effectiveValue
}
}
if currentValue != timeout {
let _ = _internal_setChatMessageAutoremoveTimeoutInteractively(account: self.account, peerId: peerId, timeout: timeout).start()
}
}
}
}
|> ignoreValues
}
public func updateChannelSlowModeInteractively(peerId: PeerId, timeout: Int32?) -> Signal<Void, UpdateChannelSlowModeError> {
return _internal_updateChannelSlowModeInteractively(postbox: self.account.postbox, network: self.account.network, accountStateManager: self.account.stateManager, peerId: peerId, timeout: timeout)

View File

@ -38,6 +38,7 @@ public enum PresentationResourceKey: Int32 {
case itemListDownArrow
case itemListDisclosureArrow
case disclosureOptionArrowsImage
case itemListDisclosureLocked
case itemListCheckIcon
case itemListSecondaryCheckIcon
@ -331,4 +332,6 @@ public enum PresentationResourceParameterKey: Hashable {
case chatEntityKeyboardLock(color: UInt32)
case chatInputMediaPanelGridDismissImage(color: UInt32)
case statusAutoremoveIcon(isActive: Bool)
}

View File

@ -408,4 +408,10 @@ public struct PresentationResourcesChatList {
return generateTintedImage(image: UIImage(bundleImageName: "Chat List/GeneralTopicIcon"), color: theme.chatList.unreadBadgeInactiveBackgroundColor)
})
}
public static func statusAutoremoveIcon(_ theme: PresentationTheme, isActive: Bool) -> UIImage? {
return theme.image(PresentationResourceParameterKey.statusAutoremoveIcon(isActive: isActive), { theme in
return generateTintedImage(image: UIImage(bundleImageName: isActive ? "Chat List/StatusIconAutoremoveOn" : "Chat List/StatusIconAutoremoveOff"), color: isActive ? theme.list.itemAccentColor : theme.list.itemSecondaryTextColor)
})
}
}

View File

@ -33,6 +33,12 @@ public struct PresentationResourcesItemList {
})
}
public static func disclosureOptionArrowsImage(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.disclosureOptionArrowsImage.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Item List/ContextDisclosureArrow"), color: theme.list.disclosureArrowColor)
})
}
public static func disclosureLockedImage(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.itemListDisclosureLocked.rawValue, { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/SmallLock"), color: theme.list.disclosureArrowColor)

View File

@ -9,6 +9,8 @@ public func stringForFullAuthorName(message: EngineMessage, strings: Presentatio
var authorName = ""
if author.id == accountPeerId {
authorName = strings.DialogList_You
} else if author.isDeleted {
authorName = strings.User_DeletedAccount
} else {
authorName = author.compactDisplayTitle
}

View File

@ -336,7 +336,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
}
case .channelMigratedFromGroup, .groupMigratedToChannel:
attributedString = NSAttributedString(string: "", font: titleFont, textColor: primaryTextColor)
case let .messageAutoremoveTimeoutUpdated(timeout):
case let .messageAutoremoveTimeoutUpdated(timeout, _):
let authorString: String
if let author = messageMainPeer(message) {
authorString = author.compactDisplayTitle

View File

@ -0,0 +1,5 @@
<svg width="12" height="13" viewBox="0 0 12 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.5953 11.5986C4.12087 11.4918 2.82405 10.6403 2.17149 8.85384C1.77098 7.7574 2.07201 6.26373 3.0746 4.37282C3.16459 4.20303 3.68939 4.39974 3.71944 4.44691L4.50543 5.68903C4.55963 5.77404 4.79721 5.69973 4.8111 5.67179C5.03783 5.21551 5.22636 4.6273 5.37671 3.90715C5.52201 3.21113 5.59278 2.56825 5.5953 1.81662C5.59594 1.62274 5.95245 1.32054 6 1.34613C7.14662 1.96317 8.0944 2.72803 8.66171 3.48285C9.63301 4.7752 9.88777 5.77431 9.96174 6.98816C10.1296 8.28857 9.7451 9.42338 8.80824 10.3926C7.87138 11.3618 6.8004 11.7638 5.5953 11.5986Z" fill="#8E8E93"/>
<path d="M3 11L11 3" stroke="white" stroke-width="1.66" stroke-linecap="round"/>
<path d="M2 11L10 3" stroke="#8E8E93" stroke-width="1.33" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 879 B

View File

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

View File

@ -0,0 +1,3 @@
<svg width="12" height="13" viewBox="0 0 12 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.5953 11.0986C4.12087 10.9918 2.82405 10.1403 2.17149 8.35384C1.77098 7.2574 2.07201 5.76373 3.0746 3.87282C3.16459 3.70303 3.68939 3.89974 3.71944 3.94691L4.50543 5.18903C4.55963 5.27404 4.79721 5.19973 4.8111 5.17179C5.03783 4.71551 5.22636 4.1273 5.37671 3.40715C5.52201 2.71113 5.59278 2.06825 5.5953 1.31662C5.59594 1.12274 5.95245 0.820539 6 0.84613C7.14662 1.46317 8.0944 2.22803 8.66171 2.98285C9.63301 4.2752 9.88777 5.27431 9.96174 6.48816C10.1296 7.78857 9.7451 8.92338 8.80824 9.8926C7.87138 10.8618 6.8004 11.2638 5.5953 11.0986Z" fill="#007AFE"/>
</svg>

After

Width:  |  Height:  |  Size: 715 B

View File

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

View File

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

View File

@ -0,0 +1,4 @@
<svg width="12" height="16" viewBox="0 0 12 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 5.5L6 2L10 5.5" stroke="#C4C4C6" stroke-width="1.66" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 10.5L6 14L2 10.5" stroke="#C4C4C6" stroke-width="1.66" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 334 B

View File

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

View File

@ -0,0 +1,13 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1_23)">
<rect width="30" height="30" rx="7" fill="#AF52DE"/>
<path d="M17 13L22.5 7.5" stroke="white" stroke-width="1.66" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15 6.99995V4.49995" stroke="white" stroke-width="1.66" stroke-linecap="round" stroke-linejoin="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15 24C19.9706 24 24 19.9706 24 15C24 10.0294 19.9706 6 15 6C10.0294 6 6 10.0294 6 15C6 19.9706 10.0294 24 15 24ZM15 8.67C15.4584 8.67 15.83 9.0416 15.83 9.5V13.7504C16.2339 14.0191 16.5 14.4785 16.5 15C16.5 15.8284 15.8284 16.5 15 16.5C14.1716 16.5 13.5 15.8284 13.5 15C13.5 14.4785 13.7661 14.0191 14.17 13.7504V9.5C14.17 9.0416 14.5416 8.67 15 8.67Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_1_23">
<rect width="30" height="30" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 934 B

View File

@ -317,7 +317,7 @@ final class AuthorizedApplicationContext {
if let _ = threadData, let threadId = firstMessage.threadId {
chatLocation = .replyThread(ChatReplyThreadMessage(
messageId: MessageId(peerId: firstMessage.id.peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false
))
).normalized)
} else {
guard let peer = firstMessage.peers[firstMessage.id.peerId] else {
return

View File

@ -295,6 +295,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
private var hasActiveGroupCallDisposable: Disposable?
private var sendAsPeersDisposable: Disposable?
private var preloadAttachBotIconsDisposables: DisposableSet?
private var keepMessageCountersSyncrhonizedDisposable: Disposable?
private let editingMessage = ValuePromise<Float?>(nil, ignoreRepeated: true)
private let startingBot = ValuePromise<Bool>(false, ignoreRepeated: true)
@ -785,19 +786,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let _ = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat {
canSetupAutoremoveTimeout = false
} else if let group = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramGroup {
if case .creator = group.role {
if !group.hasBannedPermission(.banChangeInfo) {
canSetupAutoremoveTimeout = true
} else if case let .admin(rights, _) = group.role {
if rights.rights.contains(.canDeleteMessages) {
canSetupAutoremoveTimeout = true
}
}
} else if let user = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramUser {
if user.id != strongSelf.context.account.peerId && user.botInfo == nil {
canSetupAutoremoveTimeout = true
}
} else if let channel = strongSelf.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel {
if channel.hasPermission(.deleteAllMessages) {
if channel.hasPermission(.changeInfo) {
canSetupAutoremoveTimeout = true
}
}
@ -5670,6 +5667,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.inviteRequestsDisposable.dispose()
self.sendAsPeersDisposable?.dispose()
self.preloadAttachBotIconsDisposables?.dispose()
self.keepMessageCountersSyncrhonizedDisposable?.dispose()
}
deallocate()
}
@ -8543,19 +8541,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
currentAutoremoveTimeout = secretChat.messageAutoremoveTimeout
canSetupAutoremoveTimeout = true
} else if let group = peer as? TelegramGroup {
if case .creator = group.role {
if !group.hasBannedPermission(.banChangeInfo) {
canSetupAutoremoveTimeout = true
} else if case let .admin(rights, _) = group.role {
if rights.rights.contains(.canDeleteMessages) {
canSetupAutoremoveTimeout = true
}
}
} else if let user = peer as? TelegramUser {
if user.id != strongSelf.context.account.peerId && user.botInfo == nil {
canSetupAutoremoveTimeout = true
}
} else if let channel = peer as? TelegramChannel {
if channel.hasPermission(.deleteAllMessages) {
if channel.hasPermission(.changeInfo) {
canSetupAutoremoveTimeout = true
}
}
@ -9360,7 +9354,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let strongSelf = self else {
return
}
guard case let .peer(peerId) = strongSelf.chatLocation else {
guard let peerId = strongSelf.chatLocation.peerId else {
return
}
guard let pinnedMessage = strongSelf.presentationInterfaceState.pinnedMessage else {
@ -9404,7 +9398,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})))
}
let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: .pinnedMessages(id: pinnedMessage.message.id), botStart: nil, mode: .standard(previewing: true))
let chatLocation: ChatLocation
if let _ = strongSelf.chatLocation.threadId {
chatLocation = strongSelf.chatLocation
} else {
chatLocation = .peer(id: peerId)
}
let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: chatLocation, subject: .pinnedMessages(id: pinnedMessage.message.id), botStart: nil, mode: .standard(previewing: true))
chatController.canReadHistory.set(false)
strongSelf.chatDisplayNode.messageTransitionNode.dismissMessageReactionContexts()
@ -10020,6 +10021,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
if case let .replyThread(message) = self.chatLocation, message.isForumPost {
if self.keepMessageCountersSyncrhonizedDisposable == nil {
self.keepMessageCountersSyncrhonizedDisposable = self.context.engine.messages.keepMessageCountersSyncrhonized(peerId: message.messageId.peerId, threadId: Int64(message.messageId.id)).start()
}
}
if let scheduledActivateInput = scheduledActivateInput, case .text = scheduledActivateInput {
self.scheduledActivateInput = nil
@ -14998,16 +15005,27 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
if isPinnedMessages, let messageId = messageLocation.messageId {
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
let _ = (combineLatest(
self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId)),
self.context.engine.messages.getMessagesLoadIfNecessary([messageId], strategy: .local)
)
|> deliverOnMainQueue).start(next: { [weak self] peer, messages in
guard let self, let peer = peer else {
return
}
if let navigationController = self.effectiveNavigationController {
self.dismiss()
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: true, timecode: nil), keepStack: .always))
guard let navigationController = self.effectiveNavigationController else {
return
}
self.dismiss()
let navigateToLocation: NavigateToChatControllerParams.Location
if let threadId = messages.first?.threadId {
navigateToLocation = .replyThread(ChatReplyThreadMessage(messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false))
} else {
navigateToLocation = .peer(peer)
}
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: navigateToLocation, subject: .message(id: .id(messageId), highlight: true, timecode: nil), keepStack: .always))
})
} else if case let .peer(peerId) = self.chatLocation, let messageId = messageLocation.messageId, (messageId.peerId != peerId && !forceInCurrentChat) || (isScheduledMessages && messageId.id != 0 && !Namespaces.Message.allScheduled.contains(messageId.namespace)) {
let _ = (self.context.engine.data.get(

View File

@ -2368,24 +2368,57 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
})
if let loaded = displayedRange.visibleRange, let firstEntry = historyView.filteredEntries.first, let lastEntry = historyView.filteredEntries.last {
if loaded.firstIndex < 5 && historyView.originalView.laterId != nil {
if !"".isEmpty {
print("load next")
return
var mathesFirst = false
if loaded.firstIndex <= 5 {
var firstHasGroups = false
for index in (max(0, historyView.filteredEntries.count - 5) ..< historyView.filteredEntries.count).reversed() {
switch historyView.filteredEntries[index] {
case .MessageEntry:
break
case .MessageGroupEntry:
firstHasGroups = true
default:
break
}
}
if firstHasGroups {
mathesFirst = loaded.firstIndex <= 1
} else {
mathesFirst = loaded.firstIndex <= 5
}
}
var mathesLast = false
if loaded.lastIndex >= historyView.filteredEntries.count - 5 {
var lastHasGroups = false
for index in 0 ..< min(5, historyView.filteredEntries.count) {
switch historyView.filteredEntries[index] {
case .MessageEntry:
break
case .MessageGroupEntry:
lastHasGroups = true
default:
break
}
}
if lastHasGroups {
mathesLast = loaded.lastIndex >= historyView.filteredEntries.count - 1
} else {
mathesLast = loaded.lastIndex >= historyView.filteredEntries.count - 5
}
}
if mathesFirst && historyView.originalView.laterId != nil {
let locationInput: ChatHistoryLocation = .Navigation(index: .message(lastEntry.index), anchorIndex: .message(lastEntry.index), count: historyMessageCount, highlight: false)
if self.chatHistoryLocationValue?.content != locationInput {
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: locationInput, id: self.takeNextHistoryLocationId())
}
} else if loaded.firstIndex < 5, historyView.originalView.laterId == nil, !historyView.originalView.holeLater, let chatHistoryLocationValue = self.chatHistoryLocationValue, !chatHistoryLocationValue.isAtUpperBound, historyView.originalView.anchorIndex != .upperBound {
} else if mathesFirst, historyView.originalView.laterId == nil, !historyView.originalView.holeLater, let chatHistoryLocationValue = self.chatHistoryLocationValue, !chatHistoryLocationValue.isAtUpperBound, historyView.originalView.anchorIndex != .upperBound {
if self.chatHistoryLocationValue == historyView.locationInput {
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Navigation(index: .upperBound, anchorIndex: .upperBound, count: historyMessageCount, highlight: false), id: self.takeNextHistoryLocationId())
}
} else if loaded.lastIndex >= historyView.filteredEntries.count - 5 && historyView.originalView.earlierId != nil {
if !"".isEmpty {
print("load previous")
return
}
} else if mathesLast && historyView.originalView.earlierId != nil {
let locationInput: ChatHistoryLocation = .Navigation(index: .message(firstEntry.index), anchorIndex: .message(firstEntry.index), count: historyMessageCount, highlight: false)
if self.chatHistoryLocationValue?.content != locationInput {
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: locationInput, id: self.takeNextHistoryLocationId())

View File

@ -295,19 +295,15 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte
canSetupAutoremoveTimeout = true
}
} else if let group = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramGroup {
if case .creator = group.role {
if !group.hasBannedPermission(.banChangeInfo) {
canSetupAutoremoveTimeout = true
} else if case let .admin(rights, _) = group.role {
if rights.rights.contains(.canDeleteMessages) {
canSetupAutoremoveTimeout = true
}
}
} else if let user = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramUser {
if user.botInfo == nil {
canSetupAutoremoveTimeout = true
}
} else if let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel {
if channel.hasPermission(.deleteAllMessages) {
if channel.hasPermission(.changeInfo) {
canSetupAutoremoveTimeout = true
}
}

View File

@ -93,7 +93,10 @@ func rightNavigationButtonForChatInterfaceState(_ presentationInterfaceState: Ch
if let channel = presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isForum), let moreInfoNavigationButton = moreInfoNavigationButton {
if case .replyThread = presentationInterfaceState.chatLocation {
} else {
return moreInfoNavigationButton
if case .pinnedMessages = presentationInterfaceState.subject {
} else {
return moreInfoNavigationButton
}
}
}

View File

@ -28,6 +28,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
private let params: ContactMultiselectionControllerParams
private let context: AccountContext
private let mode: ContactMultiselectionControllerMode
private let isPeerEnabled: ((EnginePeer) -> Bool)?
private let titleView: CounterContollerTitleView
@ -83,6 +84,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
self.params = params
self.context = params.context
self.mode = params.mode
self.isPeerEnabled = params.isPeerEnabled
self.options = params.options
self.filters = params.filters
self.limit = params.limit
@ -127,7 +129,9 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
})
switch self.mode {
case let .chatSelection(_, selectedChats, additionalCategories, _):
case let .chatSelection(chatSelection):
let selectedChats = chatSelection.selectedChats
let additionalCategories = chatSelection.additionalCategories
let _ = (self.context.engine.data.get(
EngineDataList(
selectedChats.map(TelegramEngine.EngineData.Item.Peer.Peer.init)
@ -207,8 +211,8 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(cancelPressed))
self.navigationItem.rightBarButtonItem = self.rightNavigationButton
rightNavigationButton.isEnabled = false
case let .chatSelection(title, _, _, _):
self.titleView.title = CounterContollerTitle(title: title, counter: "")
case let .chatSelection(chatSelection):
self.titleView.title = CounterContollerTitle(title: chatSelection.title, counter: "")
let rightNavigationButton = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.rightNavigationButtonPressed))
self.rightNavigationButton = rightNavigationButton
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(cancelPressed))
@ -218,7 +222,7 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
}
override func loadDisplayNode() {
self.displayNode = ContactMultiselectionControllerNode(navigationBar: self.navigationBar, context: self.context, presentationData: self.presentationData, mode: self.mode, options: self.options, filters: self.filters, limit: self.limit, reachedSelectionLimit: self.params.reachedLimit)
self.displayNode = ContactMultiselectionControllerNode(navigationBar: self.navigationBar, context: self.context, presentationData: self.presentationData, mode: self.mode, isPeerEnabled: self.isPeerEnabled, options: self.options, filters: self.filters, limit: self.limit, reachedSelectionLimit: self.params.reachedLimit)
switch self.contactsNode.contentNode {
case let .contacts(contactsNode):
self._listReady.set(contactsNode.ready)
@ -440,8 +444,8 @@ class ContactMultiselectionControllerImpl: ViewController, ContactMultiselection
break
case let .chats(chatsNode):
var categoryToken: EditableTokenListToken?
if case let .chatSelection(_, _, additionalCategories, _) = strongSelf.mode {
if let additionalCategories = additionalCategories {
if case let .chatSelection(chatSelection) = strongSelf.mode {
if let additionalCategories = chatSelection.additionalCategories {
for i in 0 ..< additionalCategories.categories.count {
if additionalCategories.categories[i].id == id {
categoryToken = EditableTokenListToken(id: id, title: additionalCategories.categories[i].title, fixedPosition: i)

View File

@ -71,7 +71,7 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
private let animationCache: AnimationCache
private let animationRenderer: MultiAnimationRenderer
init(navigationBar: NavigationBar?, context: AccountContext, presentationData: PresentationData, mode: ContactMultiselectionControllerMode, options: [ContactListAdditionalOption], filters: [ContactListFilter], limit: Int32?, reachedSelectionLimit: ((Int32) -> Void)?) {
init(navigationBar: NavigationBar?, context: AccountContext, presentationData: PresentationData, mode: ContactMultiselectionControllerMode, isPeerEnabled: ((EnginePeer) -> Bool)?, options: [ContactListAdditionalOption], filters: [ContactListFilter], limit: Int32?, reachedSelectionLimit: ((Int32) -> Void)?) {
self.navigationBar = navigationBar
self.context = context
@ -94,9 +94,14 @@ final class ContactMultiselectionControllerNode: ASDisplayNode {
placeholder = self.presentationData.strings.Compose_TokenListPlaceholder
}
if case let .chatSelection(_, selectedChats, additionalCategories, chatListFilters) = mode {
placeholder = self.presentationData.strings.ChatListFilter_AddChatsTitle
let chatListNode = ChatListNode(context: context, location: .chatList(groupId: .root), previewing: false, fillPreloadItems: false, mode: .peers(filter: [.excludeSecretChats], isSelecting: true, additionalCategories: additionalCategories?.categories ?? [], chatListFilters: chatListFilters), theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false)
if case let .chatSelection(chatSelection) = mode {
let placeholderValue = chatSelection.searchPlaceholder
let selectedChats = chatSelection.selectedChats
let additionalCategories = chatSelection.additionalCategories
let chatListFilters = chatSelection.chatListFilters
placeholder = placeholderValue
let chatListNode = ChatListNode(context: context, location: .chatList(groupId: .root), previewing: false, fillPreloadItems: false, mode: .peers(filter: [.excludeSecretChats], isSelecting: true, additionalCategories: additionalCategories?.categories ?? [], chatListFilters: chatListFilters, displayAutoremoveTimeout: chatSelection.displayAutoremoveTimeout), isPeerEnabled: isPeerEnabled, theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false)
if let limit = limit {
chatListNode.selectionLimit = limit
chatListNode.reachedSelectionLimit = reachedSelectionLimit

View File

@ -26,6 +26,9 @@ import MapResourceToAvatarSizes
import ItemListAddressItem
import ItemListVenueItem
import LegacyMediaPickerUI
import ContextUI
import ChatTimerScreen
import AsyncDisplayKit
private struct CreateGroupArguments {
let context: AccountContext
@ -35,10 +38,12 @@ private struct CreateGroupArguments {
let changeProfilePhoto: () -> Void
let changeLocation: () -> Void
let updateWithVenue: (TelegramMediaMap) -> Void
let updateAutoDelete: () -> Void
}
private enum CreateGroupSection: Int32 {
case info
case autoDelete
case members
case location
case venues
@ -46,17 +51,11 @@ private enum CreateGroupSection: Int32 {
private enum CreateGroupEntryTag: ItemListItemTag {
case info
case autoDelete
func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? CreateGroupEntryTag {
switch self {
case .info:
if case .info = other {
return true
} else {
return false
}
}
return self == other
} else {
return false
}
@ -66,6 +65,8 @@ private enum CreateGroupEntryTag: ItemListItemTag {
private enum CreateGroupEntry: ItemListNodeEntry {
case groupInfo(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Peer?, ItemListAvatarAndNameInfoItemState, ItemListAvatarAndNameInfoItemUpdatingAvatar?)
case setProfilePhoto(PresentationTheme, String)
case autoDelete(title: String, value: String)
case autoDeleteInfo(String)
case member(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, Peer, PeerPresence?)
case locationHeader(PresentationTheme, String)
case location(PresentationTheme, PeerGeoLocation)
@ -78,6 +79,8 @@ private enum CreateGroupEntry: ItemListNodeEntry {
switch self {
case .groupInfo, .setProfilePhoto:
return CreateGroupSection.info.rawValue
case .autoDelete, .autoDeleteInfo:
return CreateGroupSection.autoDelete.rawValue
case .member:
return CreateGroupSection.members.rawValue
case .locationHeader, .location, .changeLocation, .locationInfo:
@ -93,8 +96,12 @@ private enum CreateGroupEntry: ItemListNodeEntry {
return 0
case .setProfilePhoto:
return 1
case .autoDelete:
return 2
case .autoDeleteInfo:
return 3
case let .member(index, _, _, _, _, _, _):
return 2 + index
return 4 + index
case .locationHeader:
return 10000
case .location:
@ -146,6 +153,18 @@ private enum CreateGroupEntry: ItemListNodeEntry {
} else {
return false
}
case let .autoDelete(title, value):
if case .autoDelete(title, value) = rhs {
return true
} else {
return false
}
case let .autoDeleteInfo(text):
if case .autoDeleteInfo(text) = rhs {
return true
} else {
return false
}
case let .member(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsNameDisplayOrder, lhsPeer, lhsPresence):
if case let .member(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsNameDisplayOrder, rhsPeer, rhsPresence) = rhs {
if lhsIndex != rhsIndex {
@ -244,6 +263,12 @@ private enum CreateGroupEntry: ItemListNodeEntry {
return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: {
arguments.changeProfilePhoto()
})
case let .autoDelete(text, value):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .optionArrows, action: {
arguments.updateAutoDelete()
}, tag: CreateGroupEntryTag.autoDelete)
case let .autoDeleteInfo(text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
case let .member(_, _, _, dateTimeFormat, nameDisplayOrder, peer, presence):
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: EnginePeer(peer), presence: presence.flatMap(EnginePeer.Presence.init), text: .presence, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in })
case let .locationHeader(_, title):
@ -273,25 +298,7 @@ private struct CreateGroupState: Equatable {
var nameSetFromVenue: Bool
var avatar: ItemListAvatarAndNameInfoItemUpdatingAvatar?
var location: PeerGeoLocation?
static func ==(lhs: CreateGroupState, rhs: CreateGroupState) -> Bool {
if lhs.creating != rhs.creating {
return false
}
if lhs.editingName != rhs.editingName {
return false
}
if lhs.nameSetFromVenue != rhs.nameSetFromVenue {
return false
}
if lhs.avatar != rhs.avatar {
return false
}
if lhs.location != rhs.location {
return false
}
return true
}
var autoremoveTimeout: Int32
}
private func createGroupEntries(presentationData: PresentationData, state: CreateGroupState, peerIds: [PeerId], view: MultiplePeersView, venues: [TelegramMediaMap]?) -> [CreateGroupEntry] {
@ -303,6 +310,16 @@ private func createGroupEntries(presentationData: PresentationData, state: Creat
entries.append(.groupInfo(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer, groupInfoState, state.avatar))
//TODO:localize
let autoRemoveText: String
if state.autoremoveTimeout == 0 {
autoRemoveText = "Off"
} else {
autoRemoveText = timeIntervalString(strings: presentationData.strings, value: state.autoremoveTimeout)
}
entries.append(.autoDelete(title: "Auto-Delete Messages", value: autoRemoveText))
entries.append(.autoDeleteInfo("Automatically delete messages sent in this group for everyone after a period of time."))
var peers: [Peer] = []
for peerId in peerIds {
if let peer = view.peers[peerId] {
@ -365,7 +382,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
location = PeerGeoLocation(latitude: latitude, longitude: longitude, address: address ?? "")
}
let initialState = CreateGroupState(creating: false, editingName: .title(title: initialTitle ?? "", type: .group), nameSetFromVenue: false, avatar: nil, location: location)
let initialState = CreateGroupState(creating: false, editingName: .title(title: initialTitle ?? "", type: .group), nameSetFromVenue: false, avatar: nil, location: location, autoremoveTimeout: 0)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
let updateState: ((CreateGroupState) -> CreateGroupState) -> Void = { f in
@ -375,9 +392,11 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
var replaceControllerImpl: ((ViewController) -> Void)?
var dismissImpl: (() -> Void)?
var presentControllerImpl: ((ViewController, Any?) -> Void)?
var presentInGlobalOverlay: ((ViewController) -> Void)?
var pushImpl: ((ViewController) -> Void)?
var endEditingImpl: (() -> Void)?
var ensureItemVisibleImpl: ((CreateGroupEntryTag, Bool) -> Void)?
var findAutoremoveReferenceNode: (() -> ItemListDisclosureItemNode?)?
let actionsDisposable = DisposableSet()
@ -422,29 +441,13 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
}
endEditingImpl?()
let autoremoveTimeout = stateValue.with({ $0 }).autoremoveTimeout
let ttlPeriod: Int32? = autoremoveTimeout == 0 ? nil : autoremoveTimeout
let createSignal: Signal<PeerId?, CreateGroupError>
switch mode {
case .generic:
if title.contains("*forum") {
createSignal = context.engine.peers.createSupergroup(title: title, description: nil)
|> map(Optional.init)
|> mapError { error -> CreateGroupError in
switch error {
case .generic:
return .generic
case .restricted:
return .restricted
case .tooMuchJoined:
return .tooMuchJoined
case .tooMuchLocationBasedGroups:
return .tooMuchLocationBasedGroups
case let .serverProvided(error):
return .serverProvided(error)
}
}
} else {
createSignal = context.engine.peers.createGroup(title: title, peerIds: peerIds, ttlPeriod: nil)
}
createSignal = context.engine.peers.createGroup(title: title, peerIds: peerIds, ttlPeriod: ttlPeriod)
case .supergroup:
createSignal = context.engine.peers.createSupergroup(title: title, description: nil)
|> map(Optional.init)
@ -821,6 +824,86 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
}
})
ensureItemVisibleImpl?(.info, true)
}, updateAutoDelete: {
var subItems: [ContextMenuItem] = []
let currentValue = stateValue.with({ $0 }).autoremoveTimeout
let applyValue: (Int32) -> Void = { value in
updateState { state in
var state = state
state.autoremoveTimeout = value
return state
}
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize
subItems.append(.action(ContextMenuActionItem(text: "Off", icon: { theme in
if currentValue == 0 {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
} else {
return nil
}
}, action: { _, f in
applyValue(0)
f(.default)
})))
subItems.append(.separator)
var presetValues: [Int32] = [
1 * 24 * 60 * 60,
7 * 24 * 60 * 60,
31 * 24 * 60 * 60
]
if currentValue != 0 && !presetValues.contains(currentValue) {
presetValues.append(currentValue)
presetValues.sort()
}
for value in presetValues {
subItems.append(.action(ContextMenuActionItem(text: timeIntervalString(strings: presentationData.strings, value: value), icon: { theme in
if currentValue == value {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
} else {
return nil
}
}, action: { _, f in
applyValue(value)
f(.default)
})))
}
//TODO:localize
subItems.append(.action(ContextMenuActionItem(text: "Set Custom Time...", icon: { _ in
return nil
}, action: { _, f in
f(.default)
let controller = ChatTimerScreen(context: context, updatedPresentationData: nil, style: .default, mode: .autoremove, currentTime: currentValue == 0 ? nil : currentValue, dismissByTapOutside: true, completion: { value in
applyValue(value)
})
endEditingImpl?()
presentControllerImpl?(controller, nil)
})))
if let sourceNode = findAutoremoveReferenceNode?() {
let items: Signal<ContextController.Items, NoError> = .single(ContextController.Items(content: .list(subItems)))
let source: ContextContentSource = .reference(CreateGroupContextReferenceContentSource(sourceView: sourceNode.labelNode.view))
let contextController = ContextController(
account: context.account,
presentationData: presentationData,
source: source,
items: items,
gesture: nil
)
sourceNode.updateHasContextMenu(hasContextMenu: true)
contextController.dismissed = { [weak sourceNode] in
sourceNode?.updateHasContextMenu(hasContextMenu: false)
}
presentInGlobalOverlay?(contextController)
}
})
let signal = combineLatest(context.sharedContext.presentationData, statePromise.get(), context.account.postbox.multiplePeersView(peerIds), .single(nil) |> then(addressPromise.get()), .single(nil) |> then(venuesPromise.get()))
@ -856,6 +939,9 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
presentControllerImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a)
}
presentInGlobalOverlay = { [weak controller] c in
controller?.presentInGlobalOverlay(c, with: nil)
}
pushImpl = { [weak controller] c in
controller?.push(c)
}
@ -888,5 +974,41 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
}
})
}
findAutoremoveReferenceNode = { [weak controller] in
guard let controller else {
return nil
}
let targetTag: CreateGroupEntryTag = .autoDelete
var resultItemNode: ItemListItemNode?
controller.forEachItemNode { itemNode in
if let itemNode = itemNode as? ItemListItemNode {
if let tag = itemNode.tag, tag.isEqual(to: targetTag) {
resultItemNode = itemNode
return
}
}
}
if let resultItemNode = resultItemNode as? ItemListDisclosureItemNode {
return resultItemNode
} else {
return nil
}
}
return controller
}
private final class CreateGroupContextReferenceContentSource: ContextReferenceContentSource {
private let sourceView: UIView
init(sourceView: UIView) {
self.sourceView = sourceView
}
func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, insets: UIEdgeInsets(top: -4.0, left: 0.0, bottom: -4.0, right: 0.0))
}
}

View File

@ -59,7 +59,11 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam
var isFirst = true
if params.useExisting {
for controller in params.navigationController.viewControllers.reversed() {
if let controller = controller as? ChatControllerImpl, controller.chatLocation == params.chatLocation.asChatLocation && (controller.subject != .scheduledMessages || controller.subject == params.subject) {
guard let controller = controller as? ChatControllerImpl else {
isFirst = false
continue
}
if controller.chatLocation.peerId == params.chatLocation.asChatLocation.peerId && controller.chatLocation.threadId == params.chatLocation.asChatLocation.threadId && (controller.subject != .scheduledMessages || controller.subject == params.subject) {
if let updateTextInputState = params.updateTextInputState {
controller.updateTextInputState(updateTextInputState)
}

View File

@ -459,47 +459,55 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
case let .settings(section):
dismissInput()
switch section {
case .theme:
if let navigationController = navigationController {
let controller = themeSettingsController(context: context)
case .theme:
if let navigationController = navigationController {
let controller = themeSettingsController(context: context)
controller.navigationPresentation = .modal
var controllers = navigationController.viewControllers
controllers = controllers.filter { !($0 is ThemeSettingsController) }
controllers.append(controller)
navigationController.setViewControllers(controllers, animated: true)
}
case .devices:
if let navigationController = navigationController {
let activeSessions = deferred { () -> Signal<(ActiveSessionsContext, Int, WebSessionsContext), NoError> in
let activeSessionsContext = context.engine.privacy.activeSessions()
let webSessionsContext = context.engine.privacy.webSessions()
let otherSessionCount = activeSessionsContext.state
|> map { state -> Int in
return state.sessions.filter({ !$0.isCurrent }).count
}
|> distinctUntilChanged
return otherSessionCount
|> map { value in
return (activeSessionsContext, value, webSessionsContext)
}
}
let _ = (activeSessions
|> take(1)
|> deliverOnMainQueue).start(next: { activeSessionsContext, count, webSessionsContext in
let controller = recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext, websitesOnly: false)
controller.navigationPresentation = .modal
var controllers = navigationController.viewControllers
controllers = controllers.filter { !($0 is ThemeSettingsController) }
controllers = controllers.filter { !($0 is RecentSessionsController) }
controllers.append(controller)
navigationController.setViewControllers(controllers, animated: true)
}
case .devices:
if let navigationController = navigationController {
let activeSessions = deferred { () -> Signal<(ActiveSessionsContext, Int, WebSessionsContext), NoError> in
let activeSessionsContext = context.engine.privacy.activeSessions()
let webSessionsContext = context.engine.privacy.webSessions()
let otherSessionCount = activeSessionsContext.state
|> map { state -> Int in
return state.sessions.filter({ !$0.isCurrent }).count
}
|> distinctUntilChanged
return otherSessionCount
|> map { value in
return (activeSessionsContext, value, webSessionsContext)
}
}
let _ = (activeSessions
|> take(1)
|> deliverOnMainQueue).start(next: { activeSessionsContext, count, webSessionsContext in
let controller = recentSessionsController(context: context, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext, websitesOnly: false)
controller.navigationPresentation = .modal
var controllers = navigationController.viewControllers
controllers = controllers.filter { !($0 is RecentSessionsController) }
controllers.append(controller)
navigationController.setViewControllers(controllers, animated: true)
})
}
})
}
case .autoremoveMessages:
let _ = (context.engine.privacy.requestAccountPrivacySettings()
|> take(1)
|> deliverOnMainQueue).start(next: { settings in
navigationController?.pushViewController(globalAutoremoveScreen(context: context, initialValue: settings.messageAutoremoveTimeout ?? 0, updated: { _ in }), animated: true)
})
case .twoStepAuth:
break
}
case let .premiumOffer(reference):
dismissInput()

View File

@ -817,12 +817,14 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
if let path = parsedUrl.pathComponents.last {
var section: ResolvedUrlSettingsSection?
switch path {
case "themes":
section = .theme
case "devices":
section = .devices
default:
break
case "themes":
section = .theme
case "devices":
section = .devices
case "password":
section = .twoStepAuth
default:
break
}
if let section = section {
handleResolvedUrl(.settings(section))

View File

@ -2180,7 +2180,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
self.usernameNode.displaysAsynchronously = false
self.buttonsContainerNode = SparseNode()
self.buttonsContainerNode.clipsToBounds = false
self.buttonsContainerNode.clipsToBounds = true
self.regularContentNode = PeerInfoHeaderRegularContentNode()
var requestUpdateLayoutImpl: (() -> Void)?
@ -2930,10 +2930,6 @@ final class PeerInfoHeaderNode: ASDisplayNode {
}
}
if subtitleIsButton {
subtitleFrame.origin.y += 11.0
}
let singleTitleLockOffset: CGFloat = (peer?.id == self.context.account.peerId || subtitleSize.height.isZero) ? 8.0 : 0.0
let titleLockOffset: CGFloat = 7.0 + singleTitleLockOffset
@ -3028,6 +3024,19 @@ final class PeerInfoHeaderNode: ASDisplayNode {
avatarOffset = apparentTitleLockOffset + 0.0 * (1.0 - titleCollapseFraction) + 10.0 * titleCollapseFraction
}
if subtitleIsButton {
subtitleFrame.origin.y += 11.0 * (1.0 - titleCollapseFraction)
if let subtitleBackgroundButton = self.subtitleBackgroundButton {
transition.updateAlpha(node: subtitleBackgroundButton, alpha: (1.0 - titleCollapseFraction))
}
if let subtitleBackgroundNode = self.subtitleBackgroundNode {
transition.updateAlpha(node: subtitleBackgroundNode, alpha: (1.0 - titleCollapseFraction))
}
if let subtitleArrowNode = self.subtitleArrowNode {
transition.updateAlpha(node: subtitleArrowNode, alpha: (1.0 - titleCollapseFraction))
}
}
let avatarCornerRadius: CGFloat
if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) {
avatarCornerRadius = floor(avatarSize * 0.25)

View File

@ -4436,19 +4436,15 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
currentAutoremoveTimeout = secretChat.messageAutoremoveTimeout
canSetupAutoremoveTimeout = false
} else if let group = chatPeer as? TelegramGroup {
if case .creator = group.role {
if !group.hasBannedPermission(.banChangeInfo) {
canSetupAutoremoveTimeout = true
} else if case let .admin(rights, _) = group.role {
if rights.rights.contains(.canDeleteMessages) {
canSetupAutoremoveTimeout = true
}
}
} else if let user = chatPeer as? TelegramUser {
if user.id != strongSelf.context.account.peerId && user.botInfo == nil {
canSetupAutoremoveTimeout = true
}
} else if let channel = chatPeer as? TelegramChannel {
if channel.hasPermission(.deleteAllMessages) {
if channel.hasPermission(.changeInfo) {
canSetupAutoremoveTimeout = true
}
}
@ -4660,8 +4656,20 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
subItems.append(.separator)
subItems.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PeerInfo_AutoDeleteInfo, textLayout: .multiline, textFont: .small, icon: { _ in
subItems.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PeerInfo_AutoDeleteInfo + "\n\n" + strongSelf.presentationData.strings.AutoremoveSetup_AdditionalGlobalSettingsInfo, textLayout: .multiline, textFont: .small, parseMarkdown: true, icon: { _ in
return nil
}, textLinkAction: { [weak c] in
c?.dismiss(completion: nil)
guard let self else {
return
}
self.context.sharedContext.openResolvedUrl(.settings(.autoremoveMessages), context: self.context, urlContext: .generic, navigationController: self.controller?.navigationController as? NavigationController, forceExternal: false, openPeer: { _, _ in }, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { _, _ in }, dismissInput: { [weak self] in
guard let self else {
return
}
self.controller?.view.endEditing(true)
}, contentContext: nil)
}, action: nil as ((ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void)?)))
c.pushItems(items: .single(ContextController.Items(content: .list(subItems))))
@ -4804,9 +4812,27 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
subItems.append(.separator)
subItems.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PeerInfo_AutoDeleteInfo, textLayout: .multiline, textFont: .small, icon: { _ in
return nil
}, action: nil as ((ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void)?)))
if case .group = channel.info {
subItems.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PeerInfo_AutoDeleteInfo + "\n\n" + strongSelf.presentationData.strings.AutoremoveSetup_AdditionalGlobalSettingsInfo, textLayout: .multiline, textFont: .small, parseMarkdown: true, icon: { _ in
return nil
}, textLinkAction: { [weak c] in
c?.dismiss(completion: nil)
guard let self else {
return
}
self.context.sharedContext.openResolvedUrl(.settings(.autoremoveMessages), context: self.context, urlContext: .generic, navigationController: self.controller?.navigationController as? NavigationController, forceExternal: false, openPeer: { _, _ in }, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { _, _ in }, dismissInput: { [weak self] in
guard let self else {
return
}
self.controller?.view.endEditing(true)
}, contentContext: nil)
}, action: nil as ((ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void)?)))
} else {
subItems.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PeerInfo_AutoDeleteInfo, textLayout: .multiline, textFont: .small, icon: { _ in
return nil
}, action: nil as ((ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void)?)))
}
c.pushItems(items: .single(ContextController.Items(content: .list(subItems))))
})))
@ -4920,8 +4946,20 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
subItems.append(.separator)
subItems.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PeerInfo_AutoDeleteInfo, textLayout: .multiline, textFont: .small, icon: { _ in
subItems.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PeerInfo_AutoDeleteInfo + "\n\n" + strongSelf.presentationData.strings.AutoremoveSetup_AdditionalGlobalSettingsInfo, textLayout: .multiline, textFont: .small, parseMarkdown: true, icon: { _ in
return nil
}, textLinkAction: { [weak c] in
c?.dismiss(completion: nil)
guard let self else {
return
}
self.context.sharedContext.openResolvedUrl(.settings(.autoremoveMessages), context: self.context, urlContext: .generic, navigationController: self.controller?.navigationController as? NavigationController, forceExternal: false, openPeer: { _, _ in }, sendFile: nil, sendSticker: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { _, _ in }, dismissInput: { [weak self] in
guard let self else {
return
}
self.controller?.view.endEditing(true)
}, contentContext: nil)
}, action: nil as ((ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void)?)))
c.pushItems(items: .single(ContextController.Items(content: .list(subItems))))
@ -6986,11 +7024,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
threadId = Int64(message.messageId.id)
}
var temporary = false
if self.isSettings && self.data?.globalSettings?.privacySettings?.phoneDiscoveryEnabled == false {
temporary = true
}
controller.present(ChatQrCodeScreen(context: self.context, subject: .peer(peer: peer, threadId: threadId, temporary: temporary)), in: .window(.root))
// var temporary = false
// if self.isSettings && self.data?.globalSettings?.privacySettings?.phoneDiscoveryEnabled == false {
// temporary = true
// }
controller.present(ChatQrCodeScreen(context: self.context, subject: .peer(peer: peer, threadId: threadId, temporary: false)), in: .window(.root))
}
fileprivate func openSettings(section: PeerInfoSettingsSection) {

View File

@ -137,7 +137,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
chatListLocation = .chatList(groupId: .root)
}
self.chatListNode = ChatListNode(context: context, location: chatListLocation, previewing: false, fillPreloadItems: false, mode: .peers(filter: filter, isSelecting: false, additionalCategories: chatListCategories, chatListFilters: nil), theme: self.presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false)
self.chatListNode = ChatListNode(context: context, location: chatListLocation, previewing: false, fillPreloadItems: false, mode: .peers(filter: filter, isSelecting: false, additionalCategories: chatListCategories, chatListFilters: nil, displayAutoremoveTimeout: false), theme: self.presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false)
super.init()

View File

@ -751,6 +751,8 @@ public final class OngoingCallContext {
private var signalingConnectionManager: QueueLocalObject<CallSignalingConnectionManager>?
private let audioDevice: SharedCallAudioDevice?
public static func versions(includeExperimental: Bool, includeReference: Bool) -> [(version: String, supportsVideo: Bool)] {
#if os(iOS) && DEBUG && false
if "".isEmpty {
@ -780,6 +782,14 @@ public final class OngoingCallContext {
self.logPath = logName.isEmpty ? "" : callLogsPath(account: self.account) + "/" + logName + ".log"
let logPath = self.logPath
let audioDevice: SharedCallAudioDevice?
if !"".isEmpty {
audioDevice = SharedCallAudioDevice()
} else {
audioDevice = nil
}
self.audioDevice = audioDevice
let _ = try? FileManager.default.createDirectory(atPath: callLogsPath(account: account), withIntermediateDirectories: true, attributes: nil)
self.tempStatsLogFile = EngineTempBox.shared.tempFile(fileName: "CallStats.json")
@ -900,7 +910,7 @@ public final class OngoingCallContext {
callSessionManager.sendSignalingData(internalId: internalId, data: data)
}
}
}, videoCapturer: video?.impl, preferredVideoCodec: preferredVideoCodec, audioInputDeviceId: "", useManualAudioSessionControl: true)
}, videoCapturer: video?.impl, preferredVideoCodec: preferredVideoCodec, audioInputDeviceId: "", audioDevice: audioDevice)
strongSelf.contextRef = Unmanaged.passRetained(OngoingCallThreadLocalContextHolder(context))
context.stateChanged = { [weak callSessionManager] state, videoState, remoteVideoState, remoteAudioState, remoteBatteryLevel, _ in

View File

@ -11,6 +11,16 @@
#define UIView NSView
#endif
@interface SharedCallAudioDevice : NSObject
- (instancetype _Nonnull)init;
+ (void)setupAudioSession;
- (void)setManualAudioSessionIsActive:(bool)isAudioSessionActive;
@end
@interface OngoingCallConnectionDescriptionWebrtc : NSObject
@property (nonatomic, readonly) uint8_t reflectorId;
@ -223,7 +233,7 @@ typedef NS_ENUM(int32_t, OngoingCallDataSavingWebrtc) {
sendSignalingData:(void (^ _Nonnull)(NSData * _Nonnull))sendSignalingData videoCapturer:(OngoingCallThreadLocalContextVideoCapturer * _Nullable)videoCapturer
preferredVideoCodec:(NSString * _Nullable)preferredVideoCodec
audioInputDeviceId:(NSString * _Nonnull)audioInputDeviceId
useManualAudioSessionControl:(bool)useManualAudioSessionControl;
audioDevice:(SharedCallAudioDevice * _Nullable)audioDevice;
- (void)setManualAudioSessionIsActive:(bool)isAudioSessionActive;

View File

@ -43,6 +43,95 @@
#import "platform/darwin/TGRTCCVPixelBuffer.h"
#include "rtc_base/logging.h"
namespace tgcalls {
class SharedAudioDeviceModule {
public:
virtual ~SharedAudioDeviceModule() = default;
public:
virtual rtc::scoped_refptr<webrtc::AudioDeviceModule> audioDeviceModule() = 0;
};
}
class SharedAudioDeviceModuleImpl: public tgcalls::SharedAudioDeviceModule {
public:
SharedAudioDeviceModuleImpl() {
if (tgcalls::StaticThreads::getThreads()->getWorkerThread()->IsCurrent()) {
_audioDeviceModule = rtc::make_ref_counted<webrtc::tgcalls_ios_adm::AudioDeviceModuleIOS>(false, false, 1);
} else {
tgcalls::StaticThreads::getThreads()->getWorkerThread()->BlockingCall([&]() {
_audioDeviceModule = rtc::make_ref_counted<webrtc::tgcalls_ios_adm::AudioDeviceModuleIOS>(false, false, 1);
});
}
}
virtual ~SharedAudioDeviceModuleImpl() override {
if (tgcalls::StaticThreads::getThreads()->getWorkerThread()->IsCurrent()) {
_audioDeviceModule = nullptr;
} else {
tgcalls::StaticThreads::getThreads()->getWorkerThread()->BlockingCall([&]() {
_audioDeviceModule = nullptr;
});
}
}
public:
virtual rtc::scoped_refptr<webrtc::AudioDeviceModule> audioDeviceModule() override {
return _audioDeviceModule;
}
private:
rtc::scoped_refptr<webrtc::AudioDeviceModule> _audioDeviceModule;
};
@implementation SharedCallAudioDevice {
std::shared_ptr<tgcalls::ThreadLocalObject<tgcalls::SharedAudioDeviceModule>> _audioDeviceModule;
}
- (instancetype _Nonnull)init {
self = [super init];
if (self != nil) {
_audioDeviceModule.reset(new tgcalls::ThreadLocalObject<tgcalls::SharedAudioDeviceModule>(tgcalls::StaticThreads::getThreads()->getWorkerThread(), []() mutable {
return (tgcalls::SharedAudioDeviceModule *)(new SharedAudioDeviceModuleImpl());
}));
}
return self;
}
- (void)dealloc {
_audioDeviceModule.reset();
}
- (std::shared_ptr<tgcalls::ThreadLocalObject<tgcalls::SharedAudioDeviceModule>>)getAudioDeviceModule {
return _audioDeviceModule;
}
+ (void)setupAudioSession {
RTCAudioSessionConfiguration *sharedConfiguration = [RTCAudioSessionConfiguration webRTCConfiguration];
sharedConfiguration.mode = AVAudioSessionModeVoiceChat;
sharedConfiguration.categoryOptions |= AVAudioSessionCategoryOptionMixWithOthers;
sharedConfiguration.categoryOptions |= AVAudioSessionCategoryOptionAllowBluetoothA2DP;
sharedConfiguration.outputNumberOfChannels = 1;
[RTCAudioSessionConfiguration setWebRTCConfiguration:sharedConfiguration];
[[RTCAudioSession sharedInstance] lockForConfiguration];
[[RTCAudioSession sharedInstance] setConfiguration:sharedConfiguration active:false error:nil disableRecording:false];
[[RTCAudioSession sharedInstance] unlockForConfiguration];
}
- (void)setManualAudioSessionIsActive:(bool)isAudioSessionActive {
if (isAudioSessionActive) {
[[RTCAudioSession sharedInstance] audioSessionDidActivate:[AVAudioSession sharedInstance]];
} else {
[[RTCAudioSession sharedInstance] audioSessionDidDeactivate:[AVAudioSession sharedInstance]];
}
[RTCAudioSession sharedInstance].isAudioEnabled = isAudioSessionActive;
}
@end
@implementation OngoingCallConnectionDescriptionWebrtc
- (instancetype _Nonnull)initWithReflectorId:(uint8_t)reflectorId hasStun:(bool)hasStun hasTurn:(bool)hasTurn hasTcp:(bool)hasTcp ip:(NSString * _Nonnull)ip port:(int32_t)port username:(NSString * _Nonnull)username password:(NSString * _Nonnull)password {
@ -709,6 +798,7 @@ tgcalls::VideoCaptureInterfaceObject *GetVideoCaptureAssumingSameThread(tgcalls:
int32_t _contextId;
bool _useManualAudioSessionControl;
SharedCallAudioDevice *_audioDevice;
OngoingCallNetworkTypeWebrtc _networkType;
NSTimeInterval _callReceiveTimeout;
@ -876,7 +966,7 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
sendSignalingData:(void (^ _Nonnull)(NSData * _Nonnull))sendSignalingData videoCapturer:(OngoingCallThreadLocalContextVideoCapturer * _Nullable)videoCapturer
preferredVideoCodec:(NSString * _Nullable)preferredVideoCodec
audioInputDeviceId:(NSString * _Nonnull)audioInputDeviceId
useManualAudioSessionControl:(bool)useManualAudioSessionControl {
audioDevice:(SharedCallAudioDevice * _Nullable)audioDevice {
self = [super init];
if (self != nil) {
_version = version;
@ -885,7 +975,9 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
assert([[OngoingCallThreadLocalContextWebrtc versionsWithIncludeReference:true] containsObject:version]);
_useManualAudioSessionControl = useManualAudioSessionControl;
_audioDevice = audioDevice;
_useManualAudioSessionControl = true;
[RTCAudioSession sharedInstance].useManualAudio = true;
#ifdef WEBRTC_IOS
@ -992,6 +1084,11 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
[OngoingCallThreadLocalContextWebrtc ensureRegisteredImplementations];
std::shared_ptr<tgcalls::ThreadLocalObject<tgcalls::SharedAudioDeviceModule>> audioDeviceModule;
if (_audioDevice) {
audioDeviceModule = [_audioDevice getAudioDeviceModule];
}
__weak OngoingCallThreadLocalContextWebrtc *weakSelf = self;
_tgVoip = tgcalls::Meta::Create([version UTF8String], (tgcalls::Descriptor){
.version = [version UTF8String],
@ -1116,8 +1213,12 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
}
}];
},
.createAudioDeviceModule = [](webrtc::TaskQueueFactory *taskQueueFactory) -> rtc::scoped_refptr<webrtc::AudioDeviceModule> {
return rtc::make_ref_counted<webrtc::tgcalls_ios_adm::AudioDeviceModuleIOS>(false, false, 1);
.createAudioDeviceModule = [audioDeviceModule](webrtc::TaskQueueFactory *taskQueueFactory) -> rtc::scoped_refptr<webrtc::AudioDeviceModule> {
if (audioDeviceModule) {
return audioDeviceModule->getSyncAssumingSameThread()->audioDeviceModule();
} else {
return rtc::make_ref_counted<webrtc::tgcalls_ios_adm::AudioDeviceModuleIOS>(false, false, 1);
}
}
});
_state = OngoingCallStateInitializing;

View File

@ -678,12 +678,27 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl)
}
}
} else {
return context.engine.peers.fetchForumChannelTopic(id: channel.id, threadId: Int64(messageId.id))
|> map { info -> ResolvedUrl? in
if let _ = info {
return .replyThread(messageId: messageId)
return context.engine.messages.getMessagesLoadIfNecessary([messageId], strategy: .cloud(skipLocal: false))
|> take(1)
|> mapToSignal { messages -> Signal<ResolvedUrl?, NoError> in
if let threadId = messages.first?.threadId {
return context.engine.peers.fetchForumChannelTopic(id: channel.id, threadId: threadId)
|> map { info -> ResolvedUrl? in
if let _ = info {
return .replyThreadMessage(replyThreadMessage: ChatReplyThreadMessage(messageId: MessageId(peerId: channel.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false), messageId: messageId)
} else {
return .peer(peer?._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil))
}
}
} else {
return .peer(foundPeer._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil))
return context.engine.peers.fetchForumChannelTopic(id: channel.id, threadId: Int64(messageId.id))
|> map { info -> ResolvedUrl? in
if let _ = info {
return .replyThread(messageId: messageId)
} else {
return .peer(foundPeer._asPeer(), .chat(textInputState: nil, subject: nil, peekData: nil))
}
}
}
}
}

View File

@ -325,7 +325,7 @@ func makeBridgeMedia(message: Message, strings: PresentationStrings, chatPeer: P
}
func makeBridgeChat(_ entry: ChatListEntry, strings: PresentationStrings) -> (TGBridgeChat, [Int64 : TGBridgeUser])? {
if case let .MessageEntry(index, messages, readState, _, _, renderedPeer, _, _, _, _, hasFailed, _) = entry {
if case let .MessageEntry(index, messages, readState, _, _, renderedPeer, _, _, _, _, hasFailed, _, _) = entry {
guard index.messageIndex.id.peerId.namespace != Namespaces.Peer.SecretChat else {
return nil
}