diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index a3bcd8b32c..e044f669f3 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -240,6 +240,7 @@ D08D452F1D5E340300A7428A /* Display.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D08D452A1D5E340300A7428A /* Display.framework */; }; D08D45301D5E340300A7428A /* Postbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D08D452B1D5E340300A7428A /* Postbox.framework */; }; D08D45311D5E340300A7428A /* SwiftSignalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D08D452C1D5E340300A7428A /* SwiftSignalKit.framework */; }; + D099261F1E69791E00D95539 /* GroupsInCommonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099261E1E69791E00D95539 /* GroupsInCommonController.swift */; }; D099EA1F1DE7450B001AF5A8 /* HorizontalListContextResultsChatInputContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099EA1E1DE7450B001AF5A8 /* HorizontalListContextResultsChatInputContextPanelNode.swift */; }; D099EA211DE7451D001AF5A8 /* HorizontalListContextResultsChatInputPanelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099EA201DE7451D001AF5A8 /* HorizontalListContextResultsChatInputPanelItem.swift */; }; D099EA271DE765DB001AF5A8 /* ManagedMediaId.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099EA261DE765DB001AF5A8 /* ManagedMediaId.swift */; }; @@ -717,6 +718,7 @@ D08D452B1D5E340300A7428A /* Postbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Postbox.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-diblohvjozhgaifjcniwdlixlilx/Build/Products/Debug-iphonesimulator/Postbox.framework"; sourceTree = ""; }; D08D452C1D5E340300A7428A /* SwiftSignalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftSignalKit.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-diblohvjozhgaifjcniwdlixlilx/Build/Products/Debug-iphonesimulator/SwiftSignalKit.framework"; sourceTree = ""; }; D08D452D1D5E340300A7428A /* TelegramCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TelegramCore.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-diblohvjozhgaifjcniwdlixlilx/Build/Products/Debug-iphonesimulator/TelegramCore.framework"; sourceTree = ""; }; + D099261E1E69791E00D95539 /* GroupsInCommonController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupsInCommonController.swift; sourceTree = ""; }; D099EA1E1DE7450B001AF5A8 /* HorizontalListContextResultsChatInputContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalListContextResultsChatInputContextPanelNode.swift; sourceTree = ""; }; D099EA201DE7451D001AF5A8 /* HorizontalListContextResultsChatInputPanelItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalListContextResultsChatInputPanelItem.swift; sourceTree = ""; }; D099EA261DE765DB001AF5A8 /* ManagedMediaId.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedMediaId.swift; sourceTree = ""; }; @@ -1781,6 +1783,7 @@ D0613FD41E6064D200202CDB /* ConvertToSupergroupController.swift */, D033FEAA1E61BFC100644997 /* GroupAdminsController.swift */, D0528E621E65BECA00E2FEF5 /* UserInfoController.swift */, + D099261E1E69791E00D95539 /* GroupsInCommonController.swift */, ); name = Controller; sourceTree = ""; @@ -2778,6 +2781,7 @@ D0EE971A1D88BCA0006C18E1 /* ChatInfo.swift in Sources */, D0F69DE31D6B8A420046BCD6 /* ListControllerItem.swift in Sources */, D0736F211DF41CFD00F2C02A /* ManagedAudioPlaylistPlayer.swift in Sources */, + D099261F1E69791E00D95539 /* GroupsInCommonController.swift in Sources */, D0177B801DFAE18500A5083A /* MediaPlayerTimeTextNode.swift in Sources */, D07A7DA31D957671005BCD27 /* ListMessageSnippetItemNode.swift in Sources */, D0F69E6B1D6B8C160046BCD6 /* MapInputControllerNode.swift in Sources */, diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index b4651d4862..3b44a9bf3b 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -38,6 +38,7 @@ public class ChatController: TelegramController { private var controllerInteraction: ChatControllerInteraction? private var interfaceInteraction: ChatPanelInterfaceInteraction? + private let messageContextDisposable = MetaDisposable() private let controllerNavigationDisposable = MetaDisposable() private let sentMessageEventsDisposable = MetaDisposable() private let messageActionCallbackDisposable = MetaDisposable() @@ -55,7 +56,7 @@ public class ChatController: TelegramController { private var resolveUrlDisposable: MetaDisposable? private var contextQueryState: (ChatPresentationInputQuery?, Disposable)? - private var urlPreviewQueryState: (URL?, Disposable)? + private var urlPreviewQueryState: (String?, Disposable)? private var audioRecorderValue: ManagedAudioRecorder? private var audioRecorderFeedback: HapticFeedback? @@ -336,7 +337,7 @@ public class ChatController: TelegramController { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: "")) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: "")).withUpdatedComposeDisableUrlPreview(nil) } }) } }) @@ -529,6 +530,7 @@ public class ChatController: TelegramController { self.navigationActionDisposable.dispose() self.galleryHiddenMesageAndMediaDisposable.dispose() self.peerDisposable.dispose() + self.messageContextDisposable.dispose() self.controllerNavigationDisposable.dispose() self.sentMessageEventsDisposable.dispose() self.messageActionCallbackDisposable.dispose() @@ -872,6 +874,18 @@ public class ChatController: TelegramController { } } + self.chatDisplayNode.dismissUrlPreview = { [weak self] in + if let strongSelf = self { + if let (link, _) = strongSelf.presentationInterfaceState.urlPreview { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + $0.updatedInterfaceState { + $0.withUpdatedComposeDisableUrlPreview(link) + } + }) + } + } + } + self.chatDisplayNode.navigateToLatestButton.tapped = { [weak self] in if let strongSelf = self, strongSelf.isNodeLoaded { strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() @@ -901,9 +915,53 @@ public class ChatController: TelegramController { }, deleteSelectedMessages: { [weak self] in if let strongSelf = self { if let messageIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds, !messageIds.isEmpty { - let _ = deleteMessagesInteractively(postbox: strongSelf.account.postbox, messageIds: Array(messageIds), type: .forLocalPeer).start() + strongSelf.messageContextDisposable.set((chatDeleteMessagesOptions(account: strongSelf.account, messageIds: messageIds) |> deliverOnMainQueue).start(next: { options in + if let strongSelf = self, !options.isEmpty { + let actionSheet = ActionSheetController() + var items: [ActionSheetItem] = [] + var personalPeerName: String? + var isChannel = false + if let user = strongSelf.presentationInterfaceState.peer as? TelegramUser { + personalPeerName = user.compactDisplayTitle + } else if let channel = strongSelf.presentationInterfaceState.peer as? TelegramChannel, case .broadcast = channel.info { + isChannel = true + } + + if options.contains(.globally) { + let globalTitle: String + if isChannel { + globalTitle = "Delete" + } else if let personalPeerName = personalPeerName { + globalTitle = "Delete for me and \(personalPeerName)" + } else { + globalTitle = "Delete for everyone" + } + items.append(ActionSheetButtonItem(title: globalTitle, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) + let _ = deleteMessagesInteractively(postbox: strongSelf.account.postbox, messageIds: Array(messageIds), type: .forEveryone).start() + } + })) + } + if options.contains(.locally) { + items.append(ActionSheetButtonItem(title: "Delete for me", color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) + let _ = deleteMessagesInteractively(postbox: strongSelf.account.postbox, messageIds: Array(messageIds), type: .forLocalPeer).start() + } + })) + } + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: "Cancel", color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + strongSelf.present(actionSheet, in: .window) + } + })) } - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) } }, forwardSelectedMessages: { [weak self] in if let strongSelf = self { @@ -997,7 +1055,7 @@ public class ChatController: TelegramController { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: "")) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: "")).withUpdatedComposeDisableUrlPreview(nil) } }) } }) @@ -1039,7 +1097,7 @@ public class ChatController: TelegramController { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: "")) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: "")).withUpdatedComposeDisableUrlPreview(nil) } }) } }) @@ -1316,14 +1374,22 @@ public class ChatController: TelegramController { inScopeResult = result } else { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedUrlPreview(result($0.urlPreview)) + if let updatedUrlPreviewUrl = updatedUrlPreviewUrl, let webpage = result($0.urlPreview?.1) { + return $0.updatedUrlPreview((updatedUrlPreviewUrl, webpage)) + } else { + return $0.updatedUrlPreview(nil) + } }) } } })) inScope = false if let inScopeResult = inScopeResult { - updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedUrlPreview(inScopeResult(updatedChatPresentationInterfaceState.urlPreview)) + if let updatedUrlPreviewUrl = updatedUrlPreviewUrl, let webpage = inScopeResult(updatedChatPresentationInterfaceState.urlPreview?.1) { + updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedUrlPreview((updatedUrlPreviewUrl, webpage)) + } else { + updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedUrlPreview(nil) + } } } @@ -1383,6 +1449,7 @@ public class ChatController: TelegramController { ActionSheetButtonItem(title: "Delete All Messages", color: .destructive, action: { [weak self, weak actionSheet] in actionSheet?.dismissAnimated() if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) let _ = clearHistoryInteractively(postbox: strongSelf.account.postbox, peerId: strongSelf.peerId).start() } }) @@ -1468,7 +1535,7 @@ public class ChatController: TelegramController { self.chatDisplayNode.setupSendActionOnViewUpdate({ [weak self] in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { - $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: "")) } + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: "")).withUpdatedComposeDisableUrlPreview(nil) } }) } }) diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index f8c043a6e7..7dd94e4156 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -44,6 +44,7 @@ class ChatControllerNode: ASDisplayNode { var requestUpdateChatInterfaceState: (Bool, (ChatInterfaceState) -> ChatInterfaceState) -> Void = { _ in } var displayAttachmentMenu: () -> Void = { } var updateTypingActivity: () -> Void = { } + var dismissUrlPreview: () -> Void = { } var setupSendActionOnViewUpdate: (@escaping () -> Void) -> Void = { _ in } var requestLayout: (ContainedViewLayoutTransition) -> Void = { _ in } @@ -131,7 +132,7 @@ class ChatControllerNode: ASDisplayNode { if let strongSelf = strongSelf, let textInputPanelNode = strongSelf.inputPanelNode as? ChatTextInputPanelNode { strongSelf.ignoreUpdateHeight = true textInputPanelNode.text = "" - strongSelf.requestUpdateChatInterfaceState(false, { $0.withUpdatedReplyMessageId(nil).withUpdatedForwardMessageIds(nil) }) + strongSelf.requestUpdateChatInterfaceState(false, { $0.withUpdatedReplyMessageId(nil).withUpdatedForwardMessageIds(nil).withUpdatedComposeDisableUrlPreview(nil) }) strongSelf.ignoreUpdateHeight = false } }) @@ -143,7 +144,11 @@ class ChatControllerNode: ASDisplayNode { if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } - messages.append(.message(text: text, attributes: attributes, media: nil, replyToMessageId: strongSelf.chatPresentationInterfaceState.interfaceState.replyMessageId)) + if strongSelf.chatPresentationInterfaceState.interfaceState.composeDisableUrlPreview != nil { + attributes.append(OutgoingContentInfoMessageAttribute(flags: [.disableLinkPreviews])) + } + let webpage = strongSelf.chatPresentationInterfaceState.urlPreview?.1 + messages.append(.message(text: text, attributes: attributes, media: webpage, replyToMessageId: strongSelf.chatPresentationInterfaceState.interfaceState.replyMessageId)) } if let forwardMessageIds = strongSelf.chatPresentationInterfaceState.interfaceState.forwardMessageIds { for id in forwardMessageIds { @@ -316,7 +321,7 @@ class ChatControllerNode: ASDisplayNode { } else if let _ = accessoryPanelNode as? EditAccessoryPanelNode { strongSelf.requestUpdateChatInterfaceState(true, { $0.withUpdatedEditMessage(nil) }) } else if let _ = accessoryPanelNode as? WebpagePreviewAccessoryPanelNode { - + strongSelf.dismissUrlPreview() } } } diff --git a/TelegramUI/ChatInterfaceState.swift b/TelegramUI/ChatInterfaceState.swift index e2b2ac6eba..b2e4f6f467 100644 --- a/TelegramUI/ChatInterfaceState.swift +++ b/TelegramUI/ChatInterfaceState.swift @@ -218,6 +218,7 @@ struct ChatInterfaceMessageActionsState: Coding, Equatable { final class ChatInterfaceState: PeerChatInterfaceState, Equatable { let timestamp: Int32 let composeInputState: ChatTextInputState + let composeDisableUrlPreview: String? let replyMessageId: MessageId? let forwardMessageIds: [MessageId]? let editMessage: ChatEditMessageState? @@ -243,6 +244,7 @@ final class ChatInterfaceState: PeerChatInterfaceState, Equatable { init() { self.timestamp = 0 self.composeInputState = ChatTextInputState() + self.composeDisableUrlPreview = nil self.replyMessageId = nil self.forwardMessageIds = nil self.editMessage = nil @@ -250,9 +252,10 @@ final class ChatInterfaceState: PeerChatInterfaceState, Equatable { self.messageActionsState = ChatInterfaceMessageActionsState() } - init(timestamp: Int32, composeInputState: ChatTextInputState, replyMessageId: MessageId?, forwardMessageIds: [MessageId]?, editMessage: ChatEditMessageState?, selectionState: ChatInterfaceSelectionState?, messageActionsState: ChatInterfaceMessageActionsState) { + init(timestamp: Int32, composeInputState: ChatTextInputState, composeDisableUrlPreview: String?, replyMessageId: MessageId?, forwardMessageIds: [MessageId]?, editMessage: ChatEditMessageState?, selectionState: ChatInterfaceSelectionState?, messageActionsState: ChatInterfaceMessageActionsState) { self.timestamp = timestamp self.composeInputState = composeInputState + self.composeDisableUrlPreview = composeDisableUrlPreview self.replyMessageId = replyMessageId self.forwardMessageIds = forwardMessageIds self.editMessage = editMessage @@ -267,6 +270,11 @@ final class ChatInterfaceState: PeerChatInterfaceState, Equatable { } else { self.composeInputState = ChatTextInputState() } + if let composeDisableUrlPreview = decoder.decodeStringForKey("dup") as String? { + self.composeDisableUrlPreview = composeDisableUrlPreview + } else { + self.composeDisableUrlPreview = nil + } let replyMessageIdPeerId: Int64? = decoder.decodeInt64ForKey("r.p") let replyMessageIdNamespace: Int32? = decoder.decodeInt32ForKey("r.n") let replyMessageIdId: Int32? = decoder.decodeInt32ForKey("r.i") @@ -301,6 +309,11 @@ final class ChatInterfaceState: PeerChatInterfaceState, Equatable { func encode(_ encoder: Encoder) { encoder.encodeInt32(self.timestamp, forKey: "ts") encoder.encodeObject(self.composeInputState, forKey: "is") + if let composeDisableUrlPreview = self.composeDisableUrlPreview { + encoder.encodeString(composeDisableUrlPreview, forKey: "dup") + } else { + encoder.encodeNil(forKey: "dup") + } if let replyMessageId = self.replyMessageId { encoder.encodeInt64(replyMessageId.peerId.toInt64(), forKey: "r.p") encoder.encodeInt32(replyMessageId.namespace, forKey: "r.n") @@ -343,6 +356,9 @@ final class ChatInterfaceState: PeerChatInterfaceState, Equatable { } static func ==(lhs: ChatInterfaceState, rhs: ChatInterfaceState) -> Bool { + if lhs.composeDisableUrlPreview != rhs.composeDisableUrlPreview { + return false + } if let lhsForwardMessageIds = lhs.forwardMessageIds, let rhsForwardMessageIds = rhs.forwardMessageIds { if lhsForwardMessageIds != rhsForwardMessageIds { return false @@ -359,7 +375,11 @@ final class ChatInterfaceState: PeerChatInterfaceState, Equatable { func withUpdatedComposeInputState(_ inputState: ChatTextInputState) -> ChatInterfaceState { let updatedComposeInputState = inputState - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState) + } + + func withUpdatedComposeDisableUrlPreview(_ disableUrlPreview: String?) -> ChatInterfaceState { + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: disableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState) } func withUpdatedEffectiveInputState(_ inputState: ChatTextInputState) -> ChatInterfaceState { @@ -371,15 +391,15 @@ final class ChatInterfaceState: PeerChatInterfaceState, Equatable { updatedComposeInputState = inputState } - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: updatedEditMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: updatedEditMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState) } func withUpdatedReplyMessageId(_ replyMessageId: MessageId?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, replyMessageId: replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState) } func withUpdatedForwardMessageIds(_ forwardMessageIds: [MessageId]?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState) } func withUpdatedSelectedMessage(_ messageId: MessageId) -> ChatInterfaceState { @@ -388,7 +408,7 @@ final class ChatInterfaceState: PeerChatInterfaceState, Equatable { selectedIds.formUnion(selectionState.selectedIds) } selectedIds.insert(messageId) - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState) } func withToggledSelectedMessage(_ messageId: MessageId) -> ChatInterfaceState { @@ -401,22 +421,22 @@ final class ChatInterfaceState: PeerChatInterfaceState, Equatable { } else { selectedIds.insert(messageId) } - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState) } func withoutSelectionState() -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: nil, messageActionsState: self.messageActionsState) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: nil, messageActionsState: self.messageActionsState) } func withUpdatedTimestamp(_ timestamp: Int32) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: timestamp, composeInputState: self.composeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState) + return ChatInterfaceState(timestamp: timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState) } func withUpdatedEditMessage(_ editMessage: ChatEditMessageState?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState) } func withUpdatedMessageActionsState(_ f: (ChatInterfaceMessageActionsState) -> ChatInterfaceMessageActionsState) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: f(self.messageActionsState)) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: f(self.messageActionsState)) } } diff --git a/TelegramUI/ChatInterfaceStateAccessoryPanels.swift b/TelegramUI/ChatInterfaceStateAccessoryPanels.swift index d992dd3bcb..6de2609e7a 100644 --- a/TelegramUI/ChatInterfaceStateAccessoryPanels.swift +++ b/TelegramUI/ChatInterfaceStateAccessoryPanels.swift @@ -34,12 +34,12 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS panelNode.interfaceInteraction = interfaceInteraction return panelNode } - } else if let urlPreview = chatPresentationInterfaceState.urlPreview { - if let previewPanelNode = currentPanel as? WebpagePreviewAccessoryPanelNode, previewPanelNode.webpage.id == urlPreview.id { + } else if let urlPreview = chatPresentationInterfaceState.urlPreview, chatPresentationInterfaceState.interfaceState.composeDisableUrlPreview != urlPreview.0 { + if let previewPanelNode = currentPanel as? WebpagePreviewAccessoryPanelNode, previewPanelNode.webpage.id == urlPreview.1.id { previewPanelNode.interfaceInteraction = interfaceInteraction return previewPanelNode } else { - let panelNode = WebpagePreviewAccessoryPanelNode(account: account, webpage: urlPreview) + let panelNode = WebpagePreviewAccessoryPanelNode(account: account, webpage: urlPreview.1) panelNode.interfaceInteraction = interfaceInteraction return panelNode } diff --git a/TelegramUI/ChatInterfaceStateContextMenus.swift b/TelegramUI/ChatInterfaceStateContextMenus.swift index 5165bbc587..08ba0f693a 100644 --- a/TelegramUI/ChatInterfaceStateContextMenus.swift +++ b/TelegramUI/ChatInterfaceStateContextMenus.swift @@ -3,6 +3,7 @@ import Postbox import TelegramCore import Display import UIKit +import SwiftSignalKit func contextMenuForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, message: Message, interfaceInteraction: ChatPanelInterfaceInteraction?) -> ContextMenuController? { guard let peer = chatPresentationInterfaceState.peer, let interfaceInteraction = interfaceInteraction else { @@ -23,6 +24,7 @@ func contextMenuForChatPresentationIntefaceState(_ chatPresentationInterfaceStat canReply = false } case .group: + canReply = true switch channel.role { case .creator, .editor, .moderator: canPin = true @@ -94,3 +96,83 @@ func contextMenuForChatPresentationIntefaceState(_ chatPresentationInterfaceStat return nil } } + +struct ChatDeleteMessagesOptions: OptionSet { + var rawValue: Int32 + + init(rawValue: Int32) { + self.rawValue = rawValue + } + + init() { + self.rawValue = 0 + } + + static let locally = ChatDeleteMessagesOptions(rawValue: 1 << 0) + static let globally = ChatDeleteMessagesOptions(rawValue: 1 << 1) +} + +func chatDeleteMessagesOptions(account: Account, messageIds: Set) -> Signal { + return account.postbox.modify { modifier -> ChatDeleteMessagesOptions in + var optionsMap: [MessageId: ChatDeleteMessagesOptions] = [:] + for id in messageIds { + if let peer = modifier.getPeer(id.peerId), let message = modifier.getMessage(id) { + if let channel = peer as? TelegramChannel { + var options: ChatDeleteMessagesOptions = [] + if !message.flags.contains(.Incoming) { + options.insert(.globally) + } else { + switch channel.role { + case .creator: + options.insert(.globally) + case .moderator, .editor: + options.insert(.globally) + case .member: + break + } + } + optionsMap[message.id] = options + } else if let group = peer as? TelegramGroup { + var options: ChatDeleteMessagesOptions = [] + options.insert(.locally) + if !message.flags.contains(.Incoming) { + options.insert(.globally) + } else { + switch group.role { + case .creator, .admin: + options.insert(.globally) + case .member: + break + } + } + optionsMap[message.id] = options + } else if let _ = peer as? TelegramUser { + var options: ChatDeleteMessagesOptions = [] + options.insert(.locally) + if !message.flags.contains(.Incoming) { + options.insert(.globally) + } + optionsMap[message.id] = options + } else if let _ = peer as? TelegramSecretChat { + var options: ChatDeleteMessagesOptions = [] + options.insert(.globally) + optionsMap[message.id] = options + } else { + assertionFailure() + } + } else { + optionsMap[id] = [.locally] + } + } + + if !optionsMap.isEmpty { + var reducedOptions = optionsMap.values.first! + for value in optionsMap.values { + reducedOptions.formIntersection(value) + } + return reducedOptions + } else { + return [] + } + } +} diff --git a/TelegramUI/ChatInterfaceStateContextQueries.swift b/TelegramUI/ChatInterfaceStateContextQueries.swift index e610038b0e..56d9321f09 100644 --- a/TelegramUI/ChatInterfaceStateContextQueries.swift +++ b/TelegramUI/ChatInterfaceStateContextQueries.swift @@ -182,22 +182,22 @@ func contextQueryResultStateForChatInterfacePresentationState(_ chatPresentation private let dataDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType([.link]).rawValue) -func urlPreviewStateForChatInterfacePresentationState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentQuery: URL?) -> (URL?, Signal<(TelegramMediaWebpage?) -> TelegramMediaWebpage?, NoError>)? { +func urlPreviewStateForChatInterfacePresentationState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentQuery: String?) -> (String?, Signal<(TelegramMediaWebpage?) -> TelegramMediaWebpage?, NoError>)? { if let dataDetector = dataDetector { let text = chatPresentationInterfaceState.interfaceState.composeInputState.inputText let utf16 = text.utf16 - var detectedUrl: URL? + var detectedUrl: String? let matches = dataDetector.matches(in: text, options: [], range: NSRange(location: 0, length: utf16.count)) if let match = matches.first { let urlText = (text as NSString).substring(with: match.range) - detectedUrl = URL(string: urlText) + detectedUrl = urlText } if detectedUrl != currentQuery { if let detectedUrl = detectedUrl { - return (detectedUrl, webpagePreview(account: account, url: detectedUrl.absoluteString) |> map { value in + return (detectedUrl, webpagePreview(account: account, url: detectedUrl) |> map { value in return { _ in return value } }) } else { diff --git a/TelegramUI/ChatListItem.swift b/TelegramUI/ChatListItem.swift index 3bfc811e30..71ca7e1026 100644 --- a/TelegramUI/ChatListItem.swift +++ b/TelegramUI/ChatListItem.swift @@ -596,13 +596,13 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let contentRect = rawContentRect.offsetBy(dx: editingOffset + 78.0 + revealOffset, dy: 0.0) - transition.updateFrame(node: strongSelf.dateNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateLayout.size.width, y: contentRect.origin.y + 2.0), size: dateLayout.size)) + strongSelf.dateNode.frame = CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateLayout.size.width, y: contentRect.origin.y + 2.0), size: dateLayout.size) if let statusImage = statusImage { strongSelf.statusNode.image = statusImage strongSelf.statusNode.isHidden = false let statusSize = statusImage.size - transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateLayout.size.width - 2.0 - statusSize.width, y: contentRect.origin.y + 5.0), size: statusSize)) + strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateLayout.size.width - 2.0 - statusSize.width, y: contentRect.origin.y + 5.0), size: statusSize) } else { strongSelf.statusNode.image = nil strongSelf.statusNode.isHidden = true @@ -616,8 +616,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let badgeBackgroundFrame = CGRect(x: contentRect.maxX - badgeBackgroundWidth, y: contentRect.maxY - currentBadgeBackgroundImage.size.height - 2.0, width: badgeBackgroundWidth, height: currentBadgeBackgroundImage.size.height) let badgeTextFrame = CGRect(origin: CGPoint(x: badgeBackgroundFrame.midX - badgeLayout.size.width / 2.0, y: badgeBackgroundFrame.minY + 1.0), size: badgeLayout.size) - transition.updateFrame(node: strongSelf.badgeTextNode, frame: badgeTextFrame) - transition.updateFrame(node: strongSelf.badgeBackgroundNode, frame: badgeBackgroundFrame) + strongSelf.badgeTextNode.frame = badgeTextFrame + strongSelf.badgeBackgroundNode.frame = badgeBackgroundFrame } else { strongSelf.badgeBackgroundNode.image = nil strongSelf.badgeBackgroundNode.isHidden = true @@ -632,9 +632,17 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.mutedIconNode.isHidden = true } - transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: contentRect.origin.y), size: titleLayout.size)) + let contentDeltaX = contentRect.origin.x - strongSelf.titleNode.frame.minX + strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: contentRect.origin.x, y: contentRect.origin.y), size: titleLayout.size) + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: contentRect.origin.x, y: contentRect.maxY - textLayout.size.height - 1.0), size: textLayout.size) - transition.updateFrame(node: strongSelf.textNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: contentRect.maxY - textLayout.size.height - 1.0), size: textLayout.size)) + if !contentDeltaX.isZero { + let titlePosition = strongSelf.titleNode.position + transition.animatePosition(node: strongSelf.titleNode, from: CGPoint(x: titlePosition.x - contentDeltaX, y: titlePosition.y)) + + let textPosition = strongSelf.textNode.position + transition.animatePosition(node: strongSelf.textNode, from: CGPoint(x: textPosition.x - contentDeltaX, y: textPosition.y)) + } let separatorInset: CGFloat if !nextIsPinned && item.index.pinningIndex != nil { @@ -663,7 +671,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -separatorHeight - topNegativeInset), size: CGSize(width: layout.contentSize.width, height: layout.contentSize.height + separatorHeight + topNegativeInset)) /*if crossfadeContent && animated { - if let contents = strongSelf.contentNode.contents { + if let contents = strongSelf.titleNode.contents { let tempNode = ASDisplayNode() tempNode.isLayerBacked = true tempNode.contents = contents @@ -674,9 +682,6 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { tempNode?.removeFromSupernode() }) } - } - if updateContentNode { - strongSelf.contentNode.setNeedsDisplay() }*/ strongSelf.setRevealOptions(peerRevealOptions) diff --git a/TelegramUI/ChatMediaInputNode.swift b/TelegramUI/ChatMediaInputNode.swift index 4d7b562cb3..580f3d3110 100644 --- a/TelegramUI/ChatMediaInputNode.swift +++ b/TelegramUI/ChatMediaInputNode.swift @@ -107,7 +107,7 @@ private func chatMediaInputGridEntries(view: ItemCollectionsView, recentStickers } if let recentStickers = recentStickers, !recentStickers.items.isEmpty { - let packInfo = StickerPackCollectionInfo(id: ItemCollectionId(namespace: Namespaces.ItemCollection.CloudRecentStickers, id: 0), accessHash: 0, title: "FREQUENTLY USED", shortName: "", hash: 0) + let packInfo = StickerPackCollectionInfo(id: ItemCollectionId(namespace: Namespaces.ItemCollection.CloudRecentStickers, id: 0), flags: [], accessHash: 0, title: "FREQUENTLY USED", shortName: "", hash: 0) for i in 0 ..< min(20, recentStickers.items.count) { if let item = recentStickers.items[i].contents as? RecentMediaItem, let file = item.media as? TelegramMediaFile, let mediaId = item.media.id { let index = ItemCollectionItemIndex(index: Int32(i), id: mediaId.id) diff --git a/TelegramUI/ChatPresentationInterfaceState.swift b/TelegramUI/ChatPresentationInterfaceState.swift index 0ee88dd744..e1936f2c7a 100644 --- a/TelegramUI/ChatPresentationInterfaceState.swift +++ b/TelegramUI/ChatPresentationInterfaceState.swift @@ -179,7 +179,7 @@ struct ChatPresentationInterfaceState: Equatable { let canReportPeer: Bool let chatHistoryState: ChatHistoryNodeHistoryState? let botStartPayload: String? - let urlPreview: TelegramMediaWebpage? + let urlPreview: (String, TelegramMediaWebpage)? init() { self.interfaceState = ChatInterfaceState() @@ -197,7 +197,7 @@ struct ChatPresentationInterfaceState: Equatable { self.urlPreview = nil } - init(interfaceState: ChatInterfaceState, peer: Peer?, inputTextPanelState: ChatTextInputPanelState, inputQueryResult: ChatPresentationInputQueryResult?, inputMode: ChatInputMode, titlePanelContexts: [ChatTitlePanelContext], keyboardButtonsMessage: Message?, pinnedMessageId: MessageId?, peerIsBlocked: Bool, canReportPeer: Bool, chatHistoryState: ChatHistoryNodeHistoryState?, botStartPayload: String?, urlPreview: TelegramMediaWebpage?) { + init(interfaceState: ChatInterfaceState, peer: Peer?, inputTextPanelState: ChatTextInputPanelState, inputQueryResult: ChatPresentationInputQueryResult?, inputMode: ChatInputMode, titlePanelContexts: [ChatTitlePanelContext], keyboardButtonsMessage: Message?, pinnedMessageId: MessageId?, peerIsBlocked: Bool, canReportPeer: Bool, chatHistoryState: ChatHistoryNodeHistoryState?, botStartPayload: String?, urlPreview: (String, TelegramMediaWebpage)?) { self.interfaceState = interfaceState self.peer = peer self.inputTextPanelState = inputTextPanelState @@ -273,7 +273,10 @@ struct ChatPresentationInterfaceState: Equatable { } if let lhsUrlPreview = lhs.urlPreview, let rhsUrlPreview = rhs.urlPreview { - if !lhsUrlPreview.isEqual(rhsUrlPreview) { + if lhsUrlPreview.0 != rhsUrlPreview.0 { + return false + } + if !lhsUrlPreview.1.isEqual(rhsUrlPreview.1) { return false } } else if (lhs.urlPreview != nil) != (rhs.urlPreview != nil) { @@ -331,7 +334,7 @@ struct ChatPresentationInterfaceState: Equatable { return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview) } - func updatedUrlPreview(_ urlPreview: TelegramMediaWebpage?) -> ChatPresentationInterfaceState { + func updatedUrlPreview(_ urlPreview: (String, TelegramMediaWebpage)?) -> ChatPresentationInterfaceState { return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, peerIsBlocked: self.peerIsBlocked, canReportPeer: self.canReportPeer, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: urlPreview) } } diff --git a/TelegramUI/GroupsInCommonController.swift b/TelegramUI/GroupsInCommonController.swift new file mode 100644 index 0000000000..4fe3a5bbad --- /dev/null +++ b/TelegramUI/GroupsInCommonController.swift @@ -0,0 +1,181 @@ +import Foundation +import Display +import SwiftSignalKit +import Postbox +import TelegramCore + +private final class GroupsInCommonControllerArguments { + let account: Account + + let openPeer: (PeerId) -> Void + + init(account: Account, openPeer: @escaping (PeerId) -> Void) { + self.account = account + self.openPeer = openPeer + } +} + +private enum GroupsInCommonSection: Int32 { + case peers +} + +private enum GroupsInCommonEntryStableId: Hashable { + case peer(PeerId) + + var hashValue: Int { + switch self { + case let .peer(peerId): + return peerId.hashValue + } + } + + static func ==(lhs: GroupsInCommonEntryStableId, rhs: GroupsInCommonEntryStableId) -> Bool { + switch lhs { + case let .peer(peerId): + if case .peer(peerId) = rhs { + return true + } else { + return false + } + } + } +} + +private enum GroupsInCommonEntry: ItemListNodeEntry { + case peerItem(Int32, Peer) + + var section: ItemListSectionId { + switch self { + case .peerItem: + return GroupsInCommonSection.peers.rawValue + } + } + + var stableId: GroupsInCommonEntryStableId { + switch self { + case let .peerItem(_, peer): + return .peer(peer.id) + } + } + + static func ==(lhs: GroupsInCommonEntry, rhs: GroupsInCommonEntry) -> Bool { + switch lhs { + case let .peerItem(lhsIndex, lhsPeer): + if case let .peerItem(rhsIndex, rhsPeer) = rhs { + if lhsIndex != rhsIndex { + return false + } + if !lhsPeer.isEqual(rhsPeer) { + return false + } + return true + } else { + return false + } + } + } + + static func <(lhs: GroupsInCommonEntry, rhs: GroupsInCommonEntry) -> Bool { + switch lhs { + case let .peerItem(index, _): + switch rhs { + case let .peerItem(rhsIndex, _): + return index < rhsIndex + } + } + } + + func item(_ arguments: GroupsInCommonControllerArguments) -> ListViewItem { + switch self { + case let .peerItem(_, peer): + return ItemListPeerItem(account: arguments.account, peer: peer, presence: nil, text: .none, label: nil, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, sectionId: self.section, action: { + arguments.openPeer(peer.id) + }, setPeerIdWithRevealedOptions: { _ in + }, removePeer: { _ in + }) + } + } +} + +private struct GroupsInCommonControllerState: Equatable { + static func ==(lhs: GroupsInCommonControllerState, rhs: GroupsInCommonControllerState) -> Bool { + return true + } +} + +private func groupsInCommonControllerEntries(state: GroupsInCommonControllerState, peers: [Peer]?) -> [GroupsInCommonEntry] { + var entries: [GroupsInCommonEntry] = [] + + if let peers = peers { + var index: Int32 = 0 + for peer in peers { + entries.append(.peerItem(index, peer)) + index += 1 + } + } + + return entries +} + +public func groupsInCommonController(account: Account, peerId: PeerId) -> ViewController { + let statePromise = ValuePromise(GroupsInCommonControllerState(), ignoreRepeated: true) + let stateValue = Atomic(value: GroupsInCommonControllerState()) + let updateState: ((GroupsInCommonControllerState) -> GroupsInCommonControllerState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + let actionsDisposable = DisposableSet() + + let peersPromise = Promise<[Peer]?>(nil) + + var pushControllerImpl: ((ViewController) -> Void)? + + let arguments = GroupsInCommonControllerArguments(account: account, openPeer: { memberId in + pushControllerImpl?(ChatController(account: account, peerId: memberId)) + }) + + let peersSignal: Signal<[Peer]?, NoError> = .single(nil) |> then(groupsInCommon(account: account, peerId: peerId) |> mapToSignal { peerIds -> Signal<[Peer], NoError> in + return account.postbox.modify { modifier -> [Peer] in + var result: [Peer] = [] + for id in peerIds { + if let peer = modifier.getPeer(id) { + result.append(peer) + } + } + return result + } + } + |> map { Optional($0) }) + + peersPromise.set(peersSignal) + + var previousPeers: [Peer]? + + let signal = combineLatest(statePromise.get(), peersPromise.get()) + |> deliverOnMainQueue + |> map { state, peers -> (ItemListControllerState, (ItemListNodeState, GroupsInCommonEntry.ItemGenerationArguments)) in + var emptyStateItem: ItemListControllerEmptyStateItem? + if peers == nil { + emptyStateItem = ItemListLoadingIndicatorEmptyStateItem() + } + + let previous = previousPeers + previousPeers = peers + + let controllerState = ItemListControllerState(title: "Groups in Common", leftNavigationButton: nil, rightNavigationButton: nil, animateChanges: false) + let listState = ItemListNodeState(entries: groupsInCommonControllerEntries(state: state, peers: peers), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: previous != nil && peers != nil && previous!.count >= peers!.count) + + return (controllerState, (listState, arguments)) + } |> afterDisposed { + actionsDisposable.dispose() + } + + let controller = ItemListController(signal) + pushControllerImpl = { [weak controller] c in + if let controller = controller { + (controller.navigationController as? NavigationController)?.pushViewController(c) + } + } + controller.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) + return controller +} diff --git a/TelegramUI/TransformOutgoingMessageMedia.swift b/TelegramUI/TransformOutgoingMessageMedia.swift index ea045661a3..061beb5f22 100644 --- a/TelegramUI/TransformOutgoingMessageMedia.swift +++ b/TelegramUI/TransformOutgoingMessageMedia.swift @@ -34,7 +34,7 @@ public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, me if data.complete { if file.mimeType.hasPrefix("image/") { return Signal { subscriber in - if let image = UIImage(contentsOfFile: data.path), let scaledImage = generateImage(image.size.fitted(CGSize(width: 90.0, height: 90.0)), context: { size, context in + if let image = UIImage(contentsOfFile: data.path), let scaledImage = generateImage(image.size.fitted(CGSize(width: 90.0, height: 90.0)), contextGenerator: { size, context in context.setBlendMode(.copy) context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size)) }), let thumbnailData = UIImageJPEGRepresentation(scaledImage, 0.6) { diff --git a/TelegramUI/UserInfoController.swift b/TelegramUI/UserInfoController.swift index 12e4e7ff5a..685253b334 100644 --- a/TelegramUI/UserInfoController.swift +++ b/TelegramUI/UserInfoController.swift @@ -9,14 +9,16 @@ private final class UserInfoControllerArguments { let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void let changeNotificationMuteSettings: () -> Void let openSharedMedia: () -> Void + let openGroupsInCommon: () -> Void let updatePeerBlocked: (Bool) -> Void let deleteContact: () -> Void - init(account: Account, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, changeNotificationMuteSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, updatePeerBlocked: @escaping (Bool) -> Void, deleteContact: @escaping () -> Void) { + init(account: Account, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, changeNotificationMuteSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openGroupsInCommon: @escaping () -> Void, updatePeerBlocked: @escaping (Bool) -> Void, deleteContact: @escaping () -> Void) { self.account = account self.updateEditingName = updateEditingName self.changeNotificationMuteSettings = changeNotificationMuteSettings self.openSharedMedia = openSharedMedia + self.openGroupsInCommon = openGroupsInCommon self.updatePeerBlocked = updatePeerBlocked self.deleteContact = deleteContact } @@ -40,6 +42,7 @@ private enum UserInfoEntry: ItemListNodeEntry { case sharedMedia case notifications(settings: PeerNotificationSettings?) case notificationSound(settings: PeerNotificationSettings?) + case groupsInCommon(Int32) case secretEncryptionKey(SecretChatKeyFingerprint) case block(action: DestructiveUserInfoAction) @@ -49,7 +52,7 @@ private enum UserInfoEntry: ItemListNodeEntry { return UserInfoSection.info.rawValue case .sendMessage, .shareContact, .startSecretChat: return UserInfoSection.actions.rawValue - case .sharedMedia, .notifications, .notificationSound, .secretEncryptionKey: + case .sharedMedia, .notifications, .notificationSound, .secretEncryptionKey, .groupsInCommon: return UserInfoSection.sharedMediaAndNotifications.rawValue case .block: return UserInfoSection.block.rawValue @@ -166,6 +169,12 @@ private enum UserInfoEntry: ItemListNodeEntry { default: return false } + case let .groupsInCommon(count): + if case .groupsInCommon(count) = rhs { + return true + } else { + return false + } case let .secretEncryptionKey(fingerprint): if case .secretEncryptionKey(fingerprint) = rhs { return true @@ -204,10 +213,12 @@ private enum UserInfoEntry: ItemListNodeEntry { return 1005 case .notificationSound: return 1006 - case .secretEncryptionKey: + case .groupsInCommon: return 1007 - case .block: + case .secretEncryptionKey: return 1008 + case .block: + return 1009 } } @@ -258,6 +269,10 @@ private enum UserInfoEntry: ItemListNodeEntry { label = "Default" return ItemListDisclosureItem(title: "Sound", label: label, sectionId: self.section, style: .plain, action: { }) + case let .groupsInCommon(count): + return ItemListDisclosureItem(title: "Groups in Common", label: "\(count)", sectionId: self.section, style: .plain, action: { + arguments.openGroupsInCommon() + }) case let .secretEncryptionKey(fingerprint): return ItemListDisclosureItem(title: "Encryption Key", label: "", sectionId: self.section, style: .plain, action: { }) @@ -360,11 +375,6 @@ private func userInfoEntries(account: Account, view: PeerView, state: UserInfoSt } } - var editable = true - if peer is TelegramSecretChat { - editable = false - } - if let phoneNumber = user.phone, !phoneNumber.isEmpty { entries.append(UserInfoEntry.phoneNumber(index: 0, value: PhoneNumberWithLabel(label: "home", number: phoneNumber))) } @@ -384,6 +394,9 @@ private func userInfoEntries(account: Account, view: PeerView, state: UserInfoSt entries.append(UserInfoEntry.sharedMedia) } entries.append(UserInfoEntry.notifications(settings: view.notificationSettings)) + if let groupsInCommon = (view.cachedData as? CachedUserData)?.commonGroupCount, !isEditing { + entries.append(UserInfoEntry.groupsInCommon(groupsInCommon)) + } if let _ = peer as? TelegramSecretChat { entries.append(UserInfoEntry.secretEncryptionKey(SecretChatKeyFingerprint(k0: 0, k1: 0, k2: 0, k3: 0))) @@ -486,6 +499,8 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll if let controller = peerSharedMediaController(account: account, peerId: peerId) { pushControllerImpl?(controller) } + }, openGroupsInCommon: { + pushControllerImpl?(groupsInCommonController(account: account, peerId: peerId)) }, updatePeerBlocked: { value in updatePeerBlockedDisposable.set(requestUpdatePeerIsBlocked(account: account, peerId: peerId, isBlocked: value).start()) }, deleteContact: {