diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 5e6d2a5240..03e542894f 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -196,6 +196,7 @@ D0568AAD1DF198130022E7DA /* AudioWaveformNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0568AAC1DF198130022E7DA /* AudioWaveformNode.swift */; }; D0568AAF1DF1B3920022E7DA /* HapticFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0568AAE1DF1B3920022E7DA /* HapticFeedback.swift */; }; D05811941DD5F9380057C769 /* TelegramApplicationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05811931DD5F9380057C769 /* TelegramApplicationContext.swift */; }; + D058E0CF1E8AD57300A442DE /* VideoPlayerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D058E0CE1E8AD57300A442DE /* VideoPlayerProxy.swift */; }; D05A32DC1E6EFCC2002760B4 /* NumericFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05A32DB1E6EFCC2002760B4 /* NumericFormat.swift */; }; D05A32DE1E6F0097002760B4 /* PrivacyAndSecurityController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05A32DD1E6F0097002760B4 /* PrivacyAndSecurityController.swift */; }; D05A32EA1E6F143C002760B4 /* RecentSessionsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05A32E91E6F143C002760B4 /* RecentSessionsController.swift */; }; @@ -208,6 +209,7 @@ D0613FD51E6064D200202CDB /* ConvertToSupergroupController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0613FD41E6064D200202CDB /* ConvertToSupergroupController.swift */; }; D06879551DB8F1FC00424BBD /* CachedResourceRepresentations.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06879541DB8F1FC00424BBD /* CachedResourceRepresentations.swift */; }; D06879571DB8F22200424BBD /* FetchCachedRepresentations.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06879561DB8F22200424BBD /* FetchCachedRepresentations.swift */; }; + D06E4AC41E84806300627D1D /* FetchPhotoLibraryImageResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06E4AC31E84806300627D1D /* FetchPhotoLibraryImageResource.swift */; }; D0736F211DF41CFD00F2C02A /* ManagedAudioPlaylistPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0736F201DF41CFD00F2C02A /* ManagedAudioPlaylistPlayer.swift */; }; D0736F231DF496D000F2C02A /* PeerMediaAudioPlaylist.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0736F221DF496D000F2C02A /* PeerMediaAudioPlaylist.swift */; }; D0736F251DF4D0E500F2C02A /* TelegramController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0736F241DF4D0E500F2C02A /* TelegramController.swift */; }; @@ -706,6 +708,7 @@ D0568AAC1DF198130022E7DA /* AudioWaveformNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioWaveformNode.swift; sourceTree = ""; }; D0568AAE1DF1B3920022E7DA /* HapticFeedback.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HapticFeedback.swift; sourceTree = ""; }; D05811931DD5F9380057C769 /* TelegramApplicationContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramApplicationContext.swift; sourceTree = ""; }; + D058E0CE1E8AD57300A442DE /* VideoPlayerProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerProxy.swift; sourceTree = ""; }; D05A32DB1E6EFCC2002760B4 /* NumericFormat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NumericFormat.swift; sourceTree = ""; }; D05A32DD1E6F0097002760B4 /* PrivacyAndSecurityController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivacyAndSecurityController.swift; sourceTree = ""; }; D05A32E91E6F143C002760B4 /* RecentSessionsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecentSessionsController.swift; sourceTree = ""; }; @@ -718,6 +721,7 @@ D0613FD41E6064D200202CDB /* ConvertToSupergroupController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConvertToSupergroupController.swift; sourceTree = ""; }; D06879541DB8F1FC00424BBD /* CachedResourceRepresentations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CachedResourceRepresentations.swift; sourceTree = ""; }; D06879561DB8F22200424BBD /* FetchCachedRepresentations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchCachedRepresentations.swift; sourceTree = ""; }; + D06E4AC31E84806300627D1D /* FetchPhotoLibraryImageResource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchPhotoLibraryImageResource.swift; sourceTree = ""; }; D0736F201DF41CFD00F2C02A /* ManagedAudioPlaylistPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedAudioPlaylistPlayer.swift; sourceTree = ""; }; D0736F221DF496D000F2C02A /* PeerMediaAudioPlaylist.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMediaAudioPlaylist.swift; sourceTree = ""; }; D0736F241DF4D0E500F2C02A /* TelegramController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramController.swift; sourceTree = ""; }; @@ -1907,6 +1911,7 @@ D0F69D021D6B87D30046BCD6 /* MediaPlayer.swift */, D0F69CD41D6B87D30046BCD6 /* MediaPlayerAudioRenderer.swift */, D0F69CDC1D6B87D30046BCD6 /* MediaPlayerNode.swift */, + D058E0CE1E8AD57300A442DE /* VideoPlayerProxy.swift */, D0F69D1D1D6B87D30046BCD6 /* MediaTrackDecodableFrame.swift */, D0F69D711D6B87DE0046BCD6 /* MediaTrackFrame.swift */, D0F69D701D6B87DE0046BCD6 /* MediaTrackFrameBuffer.swift */, @@ -2293,6 +2298,7 @@ D0F3A8B51E83120A00B4C64C /* FetchResource.swift */, D0F3A8B71E83125C00B4C64C /* MediaResources.swift */, D0F3A8B91E831E6300B4C64C /* FetchVideoMediaResource.swift */, + D06E4AC31E84806300627D1D /* FetchPhotoLibraryImageResource.swift */, ); name = Resources; sourceTree = ""; @@ -2606,6 +2612,7 @@ D01F66131DE8903300345CBE /* ChatTextInputAudioRecordingButton.swift in Sources */, D0F69E341D6B8B030046BCD6 /* ChatMessageForwardInfoNode.swift in Sources */, D00C7CF81E37BF680080C3D5 /* SecretChatKeyVisualization.m in Sources */, + D06E4AC41E84806300627D1D /* FetchPhotoLibraryImageResource.swift in Sources */, D0736F251DF4D0E500F2C02A /* TelegramController.swift in Sources */, D021E0AB1E3B9E2700AF709C /* ItemListRevealOptionsNode.swift in Sources */, D0F69E561D6B8BDA0046BCD6 /* GalleryControllerNode.swift in Sources */, @@ -2789,6 +2796,7 @@ D04BB2B51E44E58E00650E93 /* AuthorizationSequencePhoneEntryController.swift in Sources */, D0E305AF1E5BA8E000D7A3A2 /* ItemListLoadingIndicatorEmptyStateItem.swift in Sources */, D087750C1E3E7B7600A97350 /* PreferencesKeys.swift in Sources */, + D058E0CF1E8AD57300A442DE /* VideoPlayerProxy.swift in Sources */, D0F69E381D6B8B030046BCD6 /* ChatMessageItemView.swift in Sources */, D0D268671D78793B00C422DA /* ChatInterfaceStateNavigationButtons.swift in Sources */, D039EB081DEC725600886EBC /* ChatTextInputAudioRecordingTimeNode.swift in Sources */, diff --git a/TelegramUI/AuthorizationSequenceController.swift b/TelegramUI/AuthorizationSequenceController.swift index d5f3dd0575..2502de4bef 100644 --- a/TelegramUI/AuthorizationSequenceController.swift +++ b/TelegramUI/AuthorizationSequenceController.swift @@ -257,7 +257,7 @@ public final class AuthorizationSequenceController: NavigationController { self.setViewControllers([self.splashController(), self.signUpController(firstName: firstName, lastName: lastName)], animated: !self.viewControllers.isEmpty) } } else if let _ = state as? AuthorizedAccountState { - self._authorizedAccount.set(accountWithId(apiId: self.account.apiId, id: self.account.id, appGroupPath: self.account.appGroupPath, testingEnvironment: self.account.testingEnvironment, auxiliaryMethods: telegramAccountAuxiliaryMethods) |> mapToSignal { account -> Signal in + self._authorizedAccount.set(accountWithId(apiId: self.account.apiId, id: self.account.id, supplementary: false, appGroupPath: self.account.appGroupPath, testingEnvironment: self.account.testingEnvironment, auxiliaryMethods: telegramAccountAuxiliaryMethods) |> mapToSignal { account -> Signal in if case let .right(authorizedAccount) = account { return .single(authorizedAccount) } else { diff --git a/TelegramUI/CachedResourceRepresentations.swift b/TelegramUI/CachedResourceRepresentations.swift index 2cc821ef9f..d38e4a031e 100644 --- a/TelegramUI/CachedResourceRepresentations.swift +++ b/TelegramUI/CachedResourceRepresentations.swift @@ -45,3 +45,17 @@ final class CachedScaledImageRepresentation: CachedMediaResourceRepresentation { } } } + +final class CachedVideoFirstFrameRepresentation: CachedMediaResourceRepresentation { + var uniqueId: String { + return "first-frame" + } + + func isEqual(to: CachedMediaResourceRepresentation) -> Bool { + if to is CachedVideoFirstFrameRepresentation { + return true + } else { + return false + } + } +} diff --git a/TelegramUI/ChannelInfoController.swift b/TelegramUI/ChannelInfoController.swift index e364407997..7f38c0973f 100644 --- a/TelegramUI/ChannelInfoController.swift +++ b/TelegramUI/ChannelInfoController.swift @@ -17,8 +17,9 @@ private final class ChannelInfoControllerArguments { let reportChannel: () -> Void let leaveChannel: () -> Void let deleteChannel: () -> Void + let displayAddressNameContextMenu: (String) -> Void - init(account: Account, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updateEditingDescriptionText: @escaping (String) -> Void, openChannelTypeSetup: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openAdmins: @escaping () -> Void, openMembers: @escaping () -> Void, openBanned: @escaping () -> Void, reportChannel: @escaping () -> Void, leaveChannel: @escaping () -> Void, deleteChannel: @escaping () -> Void) { + init(account: Account, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updateEditingDescriptionText: @escaping (String) -> Void, openChannelTypeSetup: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openAdmins: @escaping () -> Void, openMembers: @escaping () -> Void, openBanned: @escaping () -> Void, reportChannel: @escaping () -> Void, leaveChannel: @escaping () -> Void, deleteChannel: @escaping () -> Void, displayAddressNameContextMenu: @escaping (String) -> Void) { self.account = account self.updateEditingName = updateEditingName self.updateEditingDescriptionText = updateEditingDescriptionText @@ -31,6 +32,7 @@ private final class ChannelInfoControllerArguments { self.reportChannel = reportChannel self.leaveChannel = leaveChannel self.deleteChannel = deleteChannel + self.displayAddressNameContextMenu = displayAddressNameContextMenu } } @@ -41,6 +43,10 @@ private enum ChannelInfoSection: ItemListSectionId { case reportOrLeave } +private enum ChannelInfoEntryTag { + case addressName +} + private enum ChannelInfoEntry: ItemListNodeEntry { case info(peer: Peer?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState) case about(text: String) @@ -189,9 +195,11 @@ private enum ChannelInfoEntry: ItemListNodeEntry { arguments.updateEditingName(editingName) }) case let .about(text): - return ItemListTextWithLabelItem(label: "about", text: text, multiline: true, sectionId: self.section) + return ItemListTextWithLabelItem(label: "about", text: text, multiline: true, sectionId: self.section, action: nil) case let .addressName(value): - return ItemListTextWithLabelItem(label: "share link", text: "https://t.me/\(value)", multiline: false, sectionId: self.section) + return ItemListTextWithLabelItem(label: "share link", text: "https://t.me/\(value)", multiline: false, sectionId: self.section, action: { + arguments.displayAddressNameContextMenu("https://t.me/\(value)") + }, tag: ChannelInfoEntryTag.addressName) case let .channelTypeSetup(isPublic): return ItemListDisclosureItem(title: "Channel Type", label: isPublic ? "Public" : "Private", sectionId: self.section, style: .plain, action: { arguments.openChannelTypeSetup() @@ -402,6 +410,7 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr var pushControllerImpl: ((ViewController) -> Void)? var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments) -> Void)? var popToRootControllerImpl: (() -> Void)? + var displayAddressNameContextMenuImpl: ((String) -> Void)? let actionsDisposable = DisposableSet() @@ -507,6 +516,8 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, deleteChannel: { + }, displayAddressNameContextMenu: { text in + displayAddressNameContextMenuImpl?(text) }) let signal = combineLatest(statePromise.get(), account.viewTracker.peerView(peerId)) @@ -620,5 +631,34 @@ public func channelInfoController(account: Account, peerId: PeerId) -> ViewContr popToRootControllerImpl = { [weak controller] in (controller?.navigationController as? NavigationController)?.popToRoot(animated: true) } + displayAddressNameContextMenuImpl = { [weak controller] text in + if let strongController = controller { + var resultItemNode: ListViewItemNode? + let _ = strongController.frameForItemNode({ itemNode in + if let itemNode = itemNode as? ItemListTextWithLabelItemNode { + if let tag = itemNode.tag as? ChannelInfoEntryTag { + if tag == .addressName { + resultItemNode = itemNode + return true + } + } + } + return false + }) + if let resultItemNode = resultItemNode { + let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text("Copy"), action: { + UIPasteboard.general.string = text + })]) + strongController.present(contextMenuController, in: .window, with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in + if let resultItemNode = resultItemNode { + return (resultItemNode, resultItemNode.contentBounds.insetBy(dx: 0.0, dy: -2.0)) + } else { + return nil + } + })) + + } + } + } return controller } diff --git a/TelegramUI/ChatBotInfoItem.swift b/TelegramUI/ChatBotInfoItem.swift index 03e6e68e47..d083732bf4 100644 --- a/TelegramUI/ChatBotInfoItem.swift +++ b/TelegramUI/ChatBotInfoItem.swift @@ -117,7 +117,7 @@ final class ChatBotInfoItemNode: ListViewItemNode { let verticalItemInset: CGFloat = 10.0 let verticalContentInset: CGFloat = 8.0 - let (textLayout, textApply) = makeTextLayout(attributedText, nil, 0, .end, CGSize(width: width - horizontalEdgeInset * 2.0 - horizontalContentInset * 2.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (textLayout, textApply) = makeTextLayout(attributedText, nil, 0, .end, CGSize(width: width - horizontalEdgeInset * 2.0 - horizontalContentInset * 2.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let backgroundFrame = CGRect(origin: CGPoint(x: floor((width - textLayout.size.width - horizontalContentInset * 2.0) / 2.0), y: verticalItemInset + 4.0), size: CGSize(width: textLayout.size.width + horizontalContentInset * 2.0, height: textLayout.size.height + verticalContentInset * 2.0)) let textFrame = CGRect(origin: CGPoint(x: backgroundFrame.origin.x + horizontalContentInset, y: backgroundFrame.origin.y + verticalContentInset), size: textLayout.size) diff --git a/TelegramUI/ChatChannelSubscriberInputPanelNode.swift b/TelegramUI/ChatChannelSubscriberInputPanelNode.swift index 02b096b7c8..9e485d71b5 100644 --- a/TelegramUI/ChatChannelSubscriberInputPanelNode.swift +++ b/TelegramUI/ChatChannelSubscriberInputPanelNode.swift @@ -37,7 +37,7 @@ private func actionForPeer(peer: Peer, muteState: PeerMuteState) -> SubscriberAc return .muteNotifications } else { return .unmuteNotifications - } + } } } else { return nil diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index 9ed54d0a97..32267fe7eb 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -78,6 +78,8 @@ public class ChatController: TelegramController { private var historyNavigationStack = ChatHistoryNavigationStack() + let canReadHistory = ValuePromise(true, ignoreRepeated: true) + public init(account: Account, peerId: PeerId, messageId: MessageId? = nil, botStart: ChatControllerInitialBotStart? = nil) { self.account = account self.peerId = peerId @@ -131,7 +133,7 @@ public class ChatController: TelegramController { } } else if let file = galleryMedia as? TelegramMediaFile, file.isMusic || file.isVoice { if let applicationContext = strongSelf.account.applicationContext as? TelegramApplicationContext { - let player = ManagedAudioPlaylistPlayer(postbox: strongSelf.account.postbox, playlist: peerMessageHistoryAudioPlaylist(account: strongSelf.account, messageId: id)) + let player = ManagedAudioPlaylistPlayer(audioSessionManager: (strongSelf.account.applicationContext as! TelegramApplicationContext).mediaManager.audioSession, postbox: strongSelf.account.postbox, playlist: peerMessageHistoryAudioPlaylist(account: strongSelf.account, messageId: id)) applicationContext.mediaManager.setPlaylistPlayer(player) player.control(.navigation(.next)) } @@ -1280,7 +1282,9 @@ public class ChatController: TelegramController { super.viewDidAppear(animated) self.chatDisplayNode.historyNode.preloadPages = true - self.chatDisplayNode.historyNode.canReadHistory.set((self.account.applicationContext as! TelegramApplicationContext).applicationInForeground) + self.chatDisplayNode.historyNode.canReadHistory.set(combineLatest((self.account.applicationContext as! TelegramApplicationContext).applicationInForeground, self.canReadHistory.get()) |> map { a, b in + return a && b + }) self.chatDisplayNode.loadInputPanels() @@ -1870,4 +1874,54 @@ public class ChatController: TelegramController { } })) } + + @available(iOSApplicationExtension 9.0, *) + override public var previewActionItems: [UIPreviewActionItem] { + struct PreviewActionsData { + let notificationSettings: PeerNotificationSettings? + let peer: Peer? + } + let peerId = self.peerId + let data = Atomic(value: nil) + let semaphore = DispatchSemaphore(value: 0) + let _ = self.account.postbox.modify({ modifier -> Void in + let _ = data.swap(PreviewActionsData(notificationSettings: modifier.getPeerNotificationSettings(peerId), peer: modifier.getPeer(peerId))) + semaphore.signal() + }).start() + semaphore.wait() + + return data.with { [weak self] data -> [UIPreviewActionItem] in + var items: [UIPreviewActionItem] = [] + if let data = data { + if let _ = data.peer as? TelegramUser { + items.append(UIPreviewAction(title: "👍", style: .default, handler: { _, _ in + if let strongSelf = self { + let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: "👍", attributes: [], media: nil, replyToMessageId: nil)]).start() + } + })) + } + + if let notificationSettings = data.notificationSettings as? TelegramPeerNotificationSettings { + if case .unmuted = notificationSettings.muteState { + let muteItem = UIPreviewAction(title: "Mute", style: .default, handler: { _, _ in + if let strongSelf = self { + let muteState: PeerMuteState = .muted(until: Int32.max) + let _ = changePeerNotificationSettings(account: strongSelf.account, peerId: strongSelf.peerId, settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: PeerMessageSound.bundledModern(id: 0))).start() + } + }) + items.append(muteItem) + } else { + let unmuteItem = UIPreviewAction(title: "Unmute", style: .default, handler: { _, _ in + if let strongSelf = self { + let muteState: PeerMuteState = .unmuted + let _ = changePeerNotificationSettings(account: strongSelf.account, peerId: strongSelf.peerId, settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: PeerMessageSound.bundledModern(id: 0))).start() + } + }) + items.append(unmuteItem) + } + } + } + return items + } + } } diff --git a/TelegramUI/ChatEmptyItem.swift b/TelegramUI/ChatEmptyItem.swift index c9dd00fa9a..2013b4b2e2 100644 --- a/TelegramUI/ChatEmptyItem.swift +++ b/TelegramUI/ChatEmptyItem.swift @@ -98,7 +98,7 @@ final class ChatEmptyItemNode: ListViewItemNode { } let imageSpacing: CGFloat = 10.0 - let (textLayout, textApply) = makeTextLayout(attributedText, nil, 0, .end, CGSize(width: width - horizontalEdgeInset * 2.0 - horizontalContentInset * 2.0, height: CGFloat.greatestFiniteMagnitude), .center, nil) + let (textLayout, textApply) = makeTextLayout(attributedText, nil, 0, .end, CGSize(width: width - horizontalEdgeInset * 2.0 - horizontalContentInset * 2.0, height: CGFloat.greatestFiniteMagnitude), .center, nil, UIEdgeInsets()) let contentWidth = max(textLayout.size.width, 120.0) diff --git a/TelegramUI/ChatHistoryEntriesForView.swift b/TelegramUI/ChatHistoryEntriesForView.swift index 0c85eef3f4..a0b00e77c6 100644 --- a/TelegramUI/ChatHistoryEntriesForView.swift +++ b/TelegramUI/ChatHistoryEntriesForView.swift @@ -9,8 +9,8 @@ func chatHistoryEntriesForView(_ view: MessageHistoryView, includeUnreadEntry: B switch entry { case let .HoleEntry(hole, _): entries.append(.HoleEntry(hole)) - case let .MessageEntry(message, read, _): - entries.append(.MessageEntry(message, read)) + case let .MessageEntry(message, read, _, monthLocation): + entries.append(.MessageEntry(message, read, monthLocation)) } } diff --git a/TelegramUI/ChatHistoryEntry.swift b/TelegramUI/ChatHistoryEntry.swift index 7ef4a178ea..8a6443def3 100644 --- a/TelegramUI/ChatHistoryEntry.swift +++ b/TelegramUI/ChatHistoryEntry.swift @@ -3,7 +3,7 @@ import TelegramCore enum ChatHistoryEntry: Identifiable, Comparable { case HoleEntry(MessageHistoryHole) - case MessageEntry(Message, Bool) + case MessageEntry(Message, Bool, MessageHistoryEntryMonthLocation?) case UnreadEntry(MessageIndex) case ChatInfoEntry(String) case EmptyChatInfoEntry @@ -12,7 +12,7 @@ enum ChatHistoryEntry: Identifiable, Comparable { switch self { case let .HoleEntry(hole): return UInt64(hole.stableId) | ((UInt64(1) << 40)) - case let .MessageEntry(message, _): + case let .MessageEntry(message, _, _): return UInt64(message.stableId) | ((UInt64(2) << 40)) case .UnreadEntry: return UInt64(3) << 40 @@ -27,7 +27,7 @@ enum ChatHistoryEntry: Identifiable, Comparable { switch self { case let .HoleEntry(hole): return hole.maxIndex - case let .MessageEntry(message, _): + case let .MessageEntry(message, _, _): return MessageIndex(message) case let .UnreadEntry(index): return index @@ -48,9 +48,9 @@ func ==(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool { default: return false } - case let .MessageEntry(lhsMessage, lhsRead): + case let .MessageEntry(lhsMessage, lhsRead, _): switch rhs { - case let .MessageEntry(rhsMessage, rhsRead) where MessageIndex(lhsMessage) == MessageIndex(rhsMessage) && lhsMessage.flags == rhsMessage.flags && lhsRead == rhsRead: + case let .MessageEntry(rhsMessage, rhsRead, _) where MessageIndex(lhsMessage) == MessageIndex(rhsMessage) && lhsMessage.flags == rhsMessage.flags && lhsRead == rhsRead: if lhsMessage.stableVersion != rhsMessage.stableVersion { return false } diff --git a/TelegramUI/ChatHistoryGridNode.swift b/TelegramUI/ChatHistoryGridNode.swift index e66abd444b..606fd96d94 100644 --- a/TelegramUI/ChatHistoryGridNode.swift +++ b/TelegramUI/ChatHistoryGridNode.swift @@ -7,17 +7,18 @@ import TelegramCore struct ChatHistoryGridViewTransition { let historyView: ChatHistoryView + let topOffsetWithinMonth: Int let deleteItems: [Int] let insertItems: [GridNodeInsertItem] let updateItems: [GridNodeUpdateItem] let scrollToItem: GridNodeScrollToItem? - let stationaryItemRange: (Int, Int)? + let stationaryItems: GridNodeStationaryItems } private func mappedInsertEntries(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, entries: [ChatHistoryViewTransitionInsertEntry]) -> [GridNodeInsertItem] { return entries.map { entry -> GridNodeInsertItem in switch entry.entry { - case let .MessageEntry(message, _): + case let .MessageEntry(message, _, _): return GridNodeInsertItem(index: entry.index, item: GridMessageItem(account: account, message: message, controllerInteraction: controllerInteraction), previousIndex: entry.previousIndex) case .HoleEntry: return GridNodeInsertItem(index: entry.index, item: GridHoleItem(), previousIndex: entry.previousIndex) @@ -34,7 +35,7 @@ private func mappedInsertEntries(account: Account, peerId: PeerId, controllerInt private func mappedUpdateEntries(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, entries: [ChatHistoryViewTransitionUpdateEntry]) -> [GridNodeUpdateItem] { return entries.map { entry -> GridNodeUpdateItem in switch entry.entry { - case let .MessageEntry(message, _): + case let .MessageEntry(message, _, _): return GridNodeUpdateItem(index: entry.index, item: GridMessageItem(account: account, message: message, controllerInteraction: controllerInteraction)) case .HoleEntry: return GridNodeUpdateItem(index: entry.index, item: GridHoleItem()) @@ -48,7 +49,7 @@ private func mappedUpdateEntries(account: Account, peerId: PeerId, controllerInt } } -private func mappedChatHistoryViewListTransition(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, transition: ChatHistoryViewTransition) -> ChatHistoryGridViewTransition { +private func mappedChatHistoryViewListTransition(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, transition: ChatHistoryViewTransition, from: ChatHistoryView?) -> ChatHistoryGridViewTransition { var mappedScrollToItem: GridNodeScrollToItem? if let scrollToItem = transition.scrollToItem { let mappedPosition: GridNodeScrollToItemPosition @@ -78,10 +79,57 @@ private func mappedChatHistoryViewListTransition(account: Account, peerId: PeerI case .Down: directionHint = .down } - mappedScrollToItem = GridNodeScrollToItem(index: scrollToItem.index, position: mappedPosition, transition: scrollTransition, directionHint: directionHint, adjustForSection: true) + mappedScrollToItem = GridNodeScrollToItem(index: scrollToItem.index, position: mappedPosition, transition: scrollTransition, directionHint: directionHint, adjustForSection: true, adjustForTopInset: true) } - return ChatHistoryGridViewTransition(historyView: transition.historyView, deleteItems: transition.deleteItems.map { $0.index }, insertItems: mappedInsertEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, entries: transition.updateEntries), scrollToItem: mappedScrollToItem, stationaryItemRange: transition.stationaryItemRange) + var stationaryItems: GridNodeStationaryItems = .none + if let previousView = from { + if let stationaryRange = transition.stationaryItemRange { + var fromStableIds = Set() + for i in 0 ..< previousView.filteredEntries.count { + if i >= stationaryRange.0 && i <= stationaryRange.1 { + fromStableIds.insert(previousView.filteredEntries[i].stableId) + } + } + var index = 0 + var indices = Set() + for entry in transition.historyView.filteredEntries { + if fromStableIds.contains(entry.stableId) { + indices.insert(transition.historyView.filteredEntries.count - 1 - index) + } + index += 1 + } + stationaryItems = .indices(indices) + } else { + var fromStableIds = Set() + for i in 0 ..< previousView.filteredEntries.count { + fromStableIds.insert(previousView.filteredEntries[i].stableId) + } + var index = 0 + var indices = Set() + for entry in transition.historyView.filteredEntries { + if fromStableIds.contains(entry.stableId) { + indices.insert(transition.historyView.filteredEntries.count - 1 - index) + } + index += 1 + } + stationaryItems = .indices(indices) + } + } + + var topOffsetWithinMonth: Int = 0 + if let lastEntry = transition.historyView.filteredEntries.last { + switch lastEntry { + case let .MessageEntry(_, _, monthLocation): + if let monthLocation = monthLocation { + topOffsetWithinMonth = Int(monthLocation.indexInMonth) + } + default: + break + } + } + + return ChatHistoryGridViewTransition(historyView: transition.historyView, topOffsetWithinMonth: topOffsetWithinMonth, deleteItems: transition.deleteItems.map { $0.index }, insertItems: mappedInsertEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, entries: transition.updateEntries), scrollToItem: mappedScrollToItem, stationaryItems: stationaryItems) } private func itemSizeForContainerLayout(size: CGSize) -> CGSize { @@ -116,7 +164,7 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { } } - private let _chatHistoryLocation = Promise() + private let _chatHistoryLocation = ValuePromise(ignoreRepeated: true) private var chatHistoryLocation: Signal { return self._chatHistoryLocation.get() } @@ -131,6 +179,8 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { super.init() + self.floatingSections = true + //self.preloadPages = false let messageViewQueue = self.messageViewQueue @@ -138,7 +188,7 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { let historyViewUpdate = self.chatHistoryLocation |> distinctUntilChanged |> mapToSignal { location in - return chatHistoryViewForLocation(location, account: account, peerId: peerId, fixedCombinedReadState: nil, tagMask: tagMask, additionalData: []) + return chatHistoryViewForLocation(location, account: account, peerId: peerId, fixedCombinedReadState: nil, tagMask: tagMask, additionalData: [], orderStatistics: [.locationWithinMonth]) } let previousView = Atomic(value: nil) @@ -179,7 +229,7 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(view, includeUnreadEntry: false, includeChatInfoEntry: false)) let previous = previousView.swap(processedView) - return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: nil, keyboardButtonsMessage: nil, cachedData: nil) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) + return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: nil, keyboardButtonsMessage: nil, cachedData: nil) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, transition: $0, from: previous) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) } } @@ -193,9 +243,21 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { self.historyDisposable.set(appliedTransition.start()) if let messageId = messageId { - self._chatHistoryLocation.set(.single(ChatHistoryLocation.InitialSearch(messageId: messageId, count: 100))) + self._chatHistoryLocation.set(ChatHistoryLocation.InitialSearch(messageId: messageId, count: 100)) } else { - self._chatHistoryLocation.set(.single(ChatHistoryLocation.Initial(count: 100))) + self._chatHistoryLocation.set(ChatHistoryLocation.Initial(count: 100)) + } + + self.visibleItemsUpdated = { [weak self] visibleItems in + if let strongSelf = self, let historyView = strongSelf.historyView, let top = visibleItems.top, let bottom = visibleItems.bottom { + if top.0 < 5 && historyView.originalView.laterId != nil { + let lastEntry = historyView.filteredEntries[historyView.filteredEntries.count - 1 - top.0] + strongSelf._chatHistoryLocation.set(ChatHistoryLocation.Navigation(index: lastEntry.index, anchorIndex: historyView.originalView.anchorIndex)) + } else if bottom.0 >= historyView.filteredEntries.count - 5 && historyView.originalView.earlierId != nil { + let firstEntry = historyView.filteredEntries[historyView.filteredEntries.count - 1 - bottom.0] + strongSelf._chatHistoryLocation.set(ChatHistoryLocation.Navigation(index: firstEntry.index, anchorIndex: historyView.originalView.anchorIndex)) + } + } } /*self.displayedItemRangeChanged = { [weak self] displayedRange in @@ -234,21 +296,20 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { } public func scrollToStartOfHistory() { - self._chatHistoryLocation.set(.single(ChatHistoryLocation.Scroll(index: MessageIndex.lowerBound(peerId: self.peerId), anchorIndex: MessageIndex.lowerBound(peerId: self.peerId), sourceIndex: MessageIndex.upperBound(peerId: self.peerId), scrollPosition: .Bottom, animated: true))) + self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: MessageIndex.lowerBound(peerId: self.peerId), anchorIndex: MessageIndex.lowerBound(peerId: self.peerId), sourceIndex: MessageIndex.upperBound(peerId: self.peerId), scrollPosition: .Bottom, animated: true)) } public func scrollToEndOfHistory() { - self._chatHistoryLocation.set(.single(ChatHistoryLocation.Scroll(index: MessageIndex.upperBound(peerId: self.peerId), anchorIndex: MessageIndex.upperBound(peerId: self.peerId), sourceIndex: MessageIndex.lowerBound(peerId: self.peerId), scrollPosition: .Top, animated: true))) + self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: MessageIndex.upperBound(peerId: self.peerId), anchorIndex: MessageIndex.upperBound(peerId: self.peerId), sourceIndex: MessageIndex.lowerBound(peerId: self.peerId), scrollPosition: .Top, animated: true)) } public func scrollToMessage(from fromIndex: MessageIndex, to toIndex: MessageIndex) { - self._chatHistoryLocation.set(.single(ChatHistoryLocation.Scroll(index: toIndex, anchorIndex: toIndex, sourceIndex: fromIndex, scrollPosition: .Center(.Bottom), animated: true))) + self._chatHistoryLocation.set(ChatHistoryLocation.Scroll(index: toIndex, anchorIndex: toIndex, sourceIndex: fromIndex, scrollPosition: .Center(.Bottom), animated: true)) } public func messageInCurrentHistoryView(_ id: MessageId) -> Message? { if let historyView = self.historyView { - var galleryMedia: Media? - for case let .MessageEntry(message, _) in historyView.filteredEntries where message.id == id { + for case let .MessageEntry(message, _, _) in historyView.filteredEntries where message.id == id { return message } } @@ -314,9 +375,9 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { updateLayout = GridNodeUpdateLayout(layout: GridNodeLayout(size: updateSizeAndInsets.size, insets: updateSizeAndInsets.insets, preloadSize: 400.0, itemSize: CGSize(width: 200.0, height: 200.0)), transition: .immediate) } - self.transaction(GridNodeTransaction(deleteItems: mappedTransition.deleteItems, insertItems: mappedTransition.insertItems, updateItems: mappedTransition.updateItems, scrollToItem: mappedTransition.scrollToItem, updateLayout: updateLayout, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: completion) + self.transaction(GridNodeTransaction(deleteItems: mappedTransition.deleteItems, insertItems: mappedTransition.insertItems, updateItems: mappedTransition.updateItems, scrollToItem: mappedTransition.scrollToItem, updateLayout: updateLayout, stationaryItems: transition.stationaryItems, updateFirstIndexInSectionOffset: mappedTransition.topOffsetWithinMonth), completion: completion) } else { - self.transaction(GridNodeTransaction(deleteItems: transition.deleteItems, insertItems: transition.insertItems, updateItems: transition.updateItems, scrollToItem: transition.scrollToItem, updateLayout: nil, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: completion) + self.transaction(GridNodeTransaction(deleteItems: transition.deleteItems, insertItems: transition.insertItems, updateItems: transition.updateItems, scrollToItem: transition.scrollToItem, updateLayout: nil, stationaryItems: transition.stationaryItems, updateFirstIndexInSectionOffset: transition.topOffsetWithinMonth), completion: completion) } } } diff --git a/TelegramUI/ChatHistoryListNode.swift b/TelegramUI/ChatHistoryListNode.swift index b4580c95cb..9e4bca9076 100644 --- a/TelegramUI/ChatHistoryListNode.swift +++ b/TelegramUI/ChatHistoryListNode.swift @@ -86,7 +86,7 @@ struct ChatHistoryListViewTransition { private func maxMessageIndexForEntries(_ entries: [ChatHistoryEntry], indexRange: (Int, Int)) -> (incoming: MessageIndex?, overall: MessageIndex?) { var overall: MessageIndex? for i in (indexRange.0 ... indexRange.1).reversed() { - if case let .MessageEntry(message, _) = entries[i] { + if case let .MessageEntry(message, _, _) = entries[i] { if overall == nil { overall = MessageIndex(message) } @@ -101,7 +101,7 @@ private func maxMessageIndexForEntries(_ entries: [ChatHistoryEntry], indexRange private func mappedInsertEntries(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, entries: [ChatHistoryViewTransitionInsertEntry]) -> [ListViewInsertItem] { return entries.map { entry -> ListViewInsertItem in switch entry.entry { - case let .MessageEntry(message, read): + case let .MessageEntry(message, read, _): let item: ListViewItem switch mode { case .bubbles: @@ -132,7 +132,7 @@ private func mappedInsertEntries(account: Account, peerId: PeerId, controllerInt private func mappedUpdateEntries(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, entries: [ChatHistoryViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { return entries.map { entry -> ListViewUpdateItem in switch entry.entry { - case let .MessageEntry(message, read): + case let .MessageEntry(message, read, _): let item: ListViewItem switch mode { case .bubbles: @@ -370,7 +370,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { var messageIdsWithViewCount: [MessageId] = [] for i in (indexRange.0 ... indexRange.1) { - if case let .MessageEntry(message, _) = historyView.filteredEntries[i] { + if case let .MessageEntry(message, _, _) = historyView.filteredEntries[i] { inner: for attribute in message.attributes { if attribute is ViewCountMessageAttribute { messageIdsWithViewCount.append(message.id) @@ -431,7 +431,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { var index = 0 for entry in historyView.filteredEntries.reversed() { if index >= visibleRange.firstIndex && index <= visibleRange.lastIndex { - if case let .MessageEntry(message, _) = entry { + if case let .MessageEntry(message, _, _) = entry { return message } } @@ -439,7 +439,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } - for case let .MessageEntry(message, _) in historyView.filteredEntries { + for case let .MessageEntry(message, _, _) in historyView.filteredEntries { return message } } @@ -448,7 +448,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { public func messageInCurrentHistoryView(_ id: MessageId) -> Message? { if let historyView = self.historyView { - for case let .MessageEntry(message, _) in historyView.filteredEntries where message.id == id { + for case let .MessageEntry(message, _, _) in historyView.filteredEntries where message.id == id { return message } } diff --git a/TelegramUI/ChatHistoryNode.swift b/TelegramUI/ChatHistoryNode.swift index f5f4908bfc..216e43943e 100644 --- a/TelegramUI/ChatHistoryNode.swift +++ b/TelegramUI/ChatHistoryNode.swift @@ -33,4 +33,5 @@ public protocol ChatHistoryNode: class { func updateLayout(transition: ContainedViewLayoutTransition, updateSizeAndInsets: ListViewUpdateSizeAndInsets) func forEachItemNode(_ f: (ASDisplayNode) -> Void) func disconnect() + func scrollToEndOfHistory() } diff --git a/TelegramUI/ChatHistoryViewForLocation.swift b/TelegramUI/ChatHistoryViewForLocation.swift index aa10573b20..f4ead4e302 100644 --- a/TelegramUI/ChatHistoryViewForLocation.swift +++ b/TelegramUI/ChatHistoryViewForLocation.swift @@ -4,16 +4,16 @@ import TelegramCore import SwiftSignalKit import Display -func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Account, peerId: PeerId, fixedCombinedReadState: CombinedPeerReadState?, tagMask: MessageTags?, additionalData: [AdditionalMessageHistoryViewData]) -> Signal { +func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Account, peerId: PeerId, fixedCombinedReadState: CombinedPeerReadState?, tagMask: MessageTags?, additionalData: [AdditionalMessageHistoryViewData], orderStatistics: MessageHistoryViewOrderStatistics = []) -> Signal { switch location { case let .Initial(count): var preloaded = false var fadeIn = false let signal: Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> if let tagMask = tagMask { - signal = account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: MessageIndex.upperBound(peerId: peerId), count: count, anchorIndex: MessageIndex.upperBound(peerId: peerId), fixedCombinedReadState: nil, tagMask: tagMask) + signal = account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: MessageIndex.upperBound(peerId: peerId), count: count, anchorIndex: MessageIndex.upperBound(peerId: peerId), fixedCombinedReadState: nil, tagMask: tagMask, orderStatistics: orderStatistics) } else { - signal = account.viewTracker.aroundUnreadMessageHistoryViewForPeerId(peerId, count: count, tagMask: tagMask, additionalData: additionalData) + signal = account.viewTracker.aroundUnreadMessageHistoryViewForPeerId(peerId, count: count, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) } return signal |> map { view, updateType, initialData -> ChatHistoryViewUpdate in var cachedData: CachedPeerData? @@ -71,7 +71,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun case let .InitialSearch(messageId, count): var preloaded = false var fadeIn = false - return account.viewTracker.aroundIdMessageHistoryViewForPeerId(peerId, count: count, messageId: messageId, tagMask: tagMask, additionalData: additionalData) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in + return account.viewTracker.aroundIdMessageHistoryViewForPeerId(peerId, count: count, messageId: messageId, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in var cachedData: CachedPeerData? for data in view.additionalData { if case let .cachedPeerData(peerIdValue, value) = data, peerIdValue == peerId { @@ -109,7 +109,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } case let .Navigation(index, anchorIndex): var first = true - return account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 140, anchorIndex: anchorIndex, fixedCombinedReadState: fixedCombinedReadState, tagMask: tagMask, additionalData: additionalData) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in + return account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 140, anchorIndex: anchorIndex, fixedCombinedReadState: fixedCombinedReadState, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in var cachedData: CachedPeerData? for data in view.additionalData { if case let .cachedPeerData(peerIdValue, value) = data, peerIdValue == peerId { @@ -131,7 +131,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up let chatScrollPosition = ChatHistoryViewScrollPosition.Index(index: index, position: scrollPosition, directionHint: directionHint, animated: animated) var first = true - return account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 140, anchorIndex: anchorIndex, fixedCombinedReadState: fixedCombinedReadState, tagMask: tagMask, additionalData: additionalData) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in + return account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 140, anchorIndex: anchorIndex, fixedCombinedReadState: fixedCombinedReadState, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in var cachedData: CachedPeerData? for data in view.additionalData { if case let .cachedPeerData(peerIdValue, value) = data, peerIdValue == peerId { diff --git a/TelegramUI/ChatHoleItem.swift b/TelegramUI/ChatHoleItem.swift index 330fcca9fa..19c596f579 100644 --- a/TelegramUI/ChatHoleItem.swift +++ b/TelegramUI/ChatHoleItem.swift @@ -80,7 +80,7 @@ class ChatHoleItemNode: ListViewItemNode { let labelLayout = TextNode.asyncLayout(self.labelNode) let layoutConstants = self.layoutConstants return { item, width, dateAtBottom in - let (size, apply) = labelLayout(NSAttributedString(string: "Loading", font: titleFont, textColor: UIColor.white), nil, 1, .end, CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (size, apply) = labelLayout(NSAttributedString(string: "Loading", font: titleFont, textColor: UIColor.white), nil, 1, .end, CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let backgroundSize = CGSize(width: size.size.width + 8.0 + 8.0, height: 20.0) diff --git a/TelegramUI/ChatListController.swift b/TelegramUI/ChatListController.swift index 8095f25d5f..d6a18f3d7b 100644 --- a/TelegramUI/ChatListController.swift +++ b/TelegramUI/ChatListController.swift @@ -10,7 +10,7 @@ private let composeButtonImage = generateImage(CGSize(width: 24.0, height: 24.0) try? drawSvgPath(context, path: "M0,4 L15,4 L14,5 L1,5 L1,22 L18,22 L18,9 L19,8 L19,23 L0,23 L0,4 Z M18.5944456,1.70209754 L19.5995507,2.70718758 L10.0510517,12.255543 L9.54849908,13.7631781 L11.0561568,13.2606331 L20.6046559,3.71227763 L21.6097611,4.71736767 L11.5587094,14.7682681 L7.53828874,15.7733582 L9.04594649,11.250453 L18.5944456,1.70209754 Z M19.0969982,1.19955251 L20.0773504,0.21921503 C20.3690844,-0.0725145755 20.8398084,-0.0729335627 21.1298838,0.217137419 L23.0947435,2.18196761 C23.3833646,2.47058439 23.3838887,2.94326675 23.0926659,3.23448517 L22.1123136,4.21482265 L19.0969982,1.19955251 Z ") }) -public class ChatListController: TelegramController { +public class ChatListController: TelegramController, UIViewControllerPreviewingDelegate { private let account: Account let openMessageFromSearchDisposable: MetaDisposable = MetaDisposable() @@ -25,6 +25,8 @@ public class ChatListController: TelegramController { private var dismissSearchOnDisappear = false + private var didSetup3dTouch = false + public override init(account: Account) { self.account = account @@ -164,8 +166,15 @@ public class ChatListController: TelegramController { self.displayNodeDidLoad() } - override public func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if !self.didSetup3dTouch { + self.didSetup3dTouch = true + if #available(iOSApplicationExtension 9.0, *) { + self.registerForPreviewing(with: self, sourceView: self.view) + } + } } override public func viewDidDisappear(_ animated: Bool) { @@ -217,5 +226,67 @@ public class ChatListController: TelegramController { @objc func composePressed() { (self.navigationController as? NavigationController)?.pushViewController(ComposeController(account: self.account)) } + + public func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? { + if let searchController = self.chatListDisplayNode.searchDisplayController { + if let (view, action) = searchController.previewViewAndActionAtLocation(location) { + if let peerId = action as? PeerId { + if #available(iOSApplicationExtension 9.0, *) { + var sourceRect = view.superview!.convert(view.frame, to: self.view) + sourceRect.size.height -= UIScreenPixel + previewingContext.sourceRect = sourceRect + } + + let chatController = ChatController(account: self.account, peerId: peerId) + chatController.canReadHistory.set(false) + chatController.containerLayoutUpdated(ContainerViewLayout(size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height - (self.view.bounds.size.height > self.view.bounds.size.width ? 50.0 : 10.0)), intrinsicInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil), transition: .immediate) + return chatController + } else if let messageId = action as? MessageId { + if #available(iOSApplicationExtension 9.0, *) { + var sourceRect = view.superview!.convert(view.frame, to: self.view) + sourceRect.size.height -= UIScreenPixel + previewingContext.sourceRect = sourceRect + } + + let chatController = ChatController(account: self.account, peerId: messageId.peerId, messageId: messageId) + chatController.canReadHistory.set(false) + chatController.containerLayoutUpdated(ContainerViewLayout(size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height - (self.view.bounds.size.height > self.view.bounds.size.width ? 50.0 : 10.0)), intrinsicInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil), transition: .immediate) + return chatController + } + } + return nil + } + + let listLocation = self.view.convert(location, to: self.chatListDisplayNode.chatListNode.view) + + var selectedNode: ChatListItemNode? + self.chatListDisplayNode.chatListNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatListItemNode, itemNode.frame.contains(listLocation) { + selectedNode = itemNode + } + } + if let selectedNode = selectedNode, let item = selectedNode.item { + if #available(iOSApplicationExtension 9.0, *) { + var sourceRect = selectedNode.view.superview!.convert(selectedNode.frame, to: self.view) + sourceRect.size.height -= UIScreenPixel + previewingContext.sourceRect = sourceRect + } + let chatController = ChatController(account: self.account, peerId: item.peer.peerId) + chatController.canReadHistory.set(false) + chatController.containerLayoutUpdated(ContainerViewLayout(size: CGSize(width: self.view.bounds.size.width, height: self.view.bounds.size.height - (self.view.bounds.size.height > self.view.bounds.size.width ? 50.0 : 10.0)), intrinsicInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil), transition: .immediate) + return chatController + } else { + return nil + } + } + + public func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController) { + if let viewControllerToCommit = viewControllerToCommit as? ViewController { + if let chatController = viewControllerToCommit as? ChatController { + chatController.canReadHistory.set(true) + } + (self.navigationController as? NavigationController)?.pushViewController(viewControllerToCommit, animated: false) + } + } } diff --git a/TelegramUI/ChatListControllerNode.swift b/TelegramUI/ChatListControllerNode.swift index 27c8ed5718..fda61c6550 100644 --- a/TelegramUI/ChatListControllerNode.swift +++ b/TelegramUI/ChatListControllerNode.swift @@ -10,7 +10,7 @@ class ChatListControllerNode: ASDisplayNode { let chatListNode: ChatListNode var navigationBar: NavigationBar? - private var searchDisplayController: SearchDisplayController? + private(set) var searchDisplayController: SearchDisplayController? private var containerLayout: (ContainerViewLayout, CGFloat)? diff --git a/TelegramUI/ChatListHoleItem.swift b/TelegramUI/ChatListHoleItem.swift index 87b3c7ad33..9dd1bc4e87 100644 --- a/TelegramUI/ChatListHoleItem.swift +++ b/TelegramUI/ChatListHoleItem.swift @@ -78,7 +78,7 @@ class ChatListHoleItemNode: ListViewItemNode { let labelNodeLayout = TextNode.asyncLayout(self.labelNode) return { width, first, last in - let (labelLayout, labelApply) = labelNodeLayout(NSAttributedString(string: "Loading", font: titleFont, textColor: UIColor(0xc8c7cc)), nil, 1, .end, CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (labelLayout, labelApply) = labelNodeLayout(NSAttributedString(string: "Loading", font: titleFont, textColor: UIColor(0xc8c7cc)), nil, 1, .end, CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let insets = ChatListItemNode.insets(first: first, last: last, firstWithHeader: false) let layout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 68.0), insets: insets) diff --git a/TelegramUI/ChatListItem.swift b/TelegramUI/ChatListItem.swift index 4b97f97db1..dcb3279225 100644 --- a/TelegramUI/ChatListItem.swift +++ b/TelegramUI/ChatListItem.swift @@ -524,9 +524,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let rawContentRect = CGRect(origin: CGPoint(x: 2.0, y: 12.0), size: CGSize(width: width - 78.0 - 10.0 - 1.0 - editingOffset, height: 68.0 - 12.0 - 9.0)) - let (dateLayout, dateApply) = dateLayout(dateAttributedString, nil, 1, .end, CGSize(width: rawContentRect.width, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (dateLayout, dateApply) = dateLayout(dateAttributedString, nil, 1, .end, CGSize(width: rawContentRect.width, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) - let (badgeLayout, badgeApply) = badgeTextLayout(badgeAttributedString, nil, 1, .end, CGSize(width: 50.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (badgeLayout, badgeApply) = badgeTextLayout(badgeAttributedString, nil, 1, .end, CGSize(width: 50.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let badgeSize: CGFloat if let currentBadgeBackgroundImage = currentBadgeBackgroundImage { @@ -535,10 +535,10 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { badgeSize = 0.0 } - let (textLayout, textApply) = textLayout(textAttributedString, nil, 1, .end, CGSize(width: rawContentRect.width - badgeSize, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (textLayout, textApply) = textLayout(textAttributedString, nil, 1, .end, CGSize(width: rawContentRect.width - badgeSize, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0)) let titleRect = CGRect(origin: rawContentRect.origin, size: CGSize(width: rawContentRect.width - dateLayout.size.width - 10.0 - statusWidth - muteWidth, height: rawContentRect.height)) - let (titleLayout, titleApply) = titleLayout(titleAttributedString, nil, 1, .end, CGSize(width: titleRect.width, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (titleLayout, titleApply) = titleLayout(titleAttributedString, nil, 1, .end, CGSize(width: titleRect.width, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let insets = ChatListItemNode.insets(first: first, last: last, firstWithHeader: firstWithHeader) let layout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 68.0), insets: insets) @@ -635,7 +635,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { 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) + strongSelf.textNode.frame = CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: contentRect.maxY - textLayout.size.height - 1.0), size: textLayout.size) if !contentDeltaX.isZero { let titlePosition = strongSelf.titleNode.position @@ -734,7 +734,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: titleFrame.origin.y), size: titleFrame.size)) let textFrame = self.textNode.frame - transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x, y: textFrame.origin.y), size: textFrame.size)) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x - 1.0, y: textFrame.origin.y), size: textFrame.size)) let dateFrame = self.dateNode.frame transition.updateFrame(node: self.dateNode, frame: CGRect(origin: CGPoint(x: contentRect.origin.x + contentRect.size.width - dateFrame.size.width, y: contentRect.origin.y + 2.0), size: dateFrame.size)) diff --git a/TelegramUI/ChatListRecentPeersListItem.swift b/TelegramUI/ChatListRecentPeersListItem.swift index 173b7ac63f..bf7586512f 100644 --- a/TelegramUI/ChatListRecentPeersListItem.swift +++ b/TelegramUI/ChatListRecentPeersListItem.swift @@ -131,4 +131,14 @@ class ChatListRecentPeersListItemNode: ListViewItemNode { return nil } } + + func viewAndPeerAtPoint(_ point: CGPoint) -> (UIView, PeerId)? { + if let peersNode = self.peersNode { + let adjustedLocation = self.convert(point, to: peersNode) + if let result = peersNode.viewAndPeerAtPoint(adjustedLocation) { + return result + } + } + return nil + } } diff --git a/TelegramUI/ChatListSearchContainerNode.swift b/TelegramUI/ChatListSearchContainerNode.swift index 7e7da457fb..d0930a86af 100644 --- a/TelegramUI/ChatListSearchContainerNode.swift +++ b/TelegramUI/ChatListSearchContainerNode.swift @@ -511,4 +511,33 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { } } } + + override func previewViewAndActionAtLocation(_ location: CGPoint) -> (UIView, Any)? { + var selectedItemNode: ASDisplayNode? + if !self.recentListNode.isHidden { + let adjustedLocation = self.convert(location, to: self.recentListNode) + self.recentListNode.forEachItemNode { itemNode in + if itemNode.frame.contains(adjustedLocation) { + selectedItemNode = itemNode + } + } + } else { + let adjustedLocation = self.convert(location, to: self.listNode) + self.listNode.forEachItemNode { itemNode in + if itemNode.frame.contains(adjustedLocation) { + selectedItemNode = itemNode + } + } + } + if let selectedItemNode = selectedItemNode as? ChatListRecentPeersListItemNode { + if let result = selectedItemNode.viewAndPeerAtPoint(self.convert(location, to: selectedItemNode)) { + return (result.0, result.1) + } + } else if let selectedItemNode = selectedItemNode as? ContactsPeerItemNode, let peer = selectedItemNode.peer { + return (selectedItemNode.view, peer.id) + } else if let selectedItemNode = selectedItemNode as? ChatListItemNode, let peerId = selectedItemNode.item?.peer.peerId { + return (selectedItemNode.view, peerId) + } + return nil + } } diff --git a/TelegramUI/ChatListSearchRecentPeersNode.swift b/TelegramUI/ChatListSearchRecentPeersNode.swift index c4d6d5c63f..21986b4712 100644 --- a/TelegramUI/ChatListSearchRecentPeersNode.swift +++ b/TelegramUI/ChatListSearchRecentPeersNode.swift @@ -54,4 +54,18 @@ final class ChatListSearchRecentPeersNode: ASDisplayNode { self.listView.position = CGPoint(x: bounds.size.width / 2.0, y: 92.0 / 2.0 + 29.0) self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: 92.0, height: bounds.size.width), insets: UIEdgeInsets(), duration: 0.0, curve: .Default), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } + + func viewAndPeerAtPoint(_ point: CGPoint) -> (UIView, PeerId)? { + let adjustedPoint = self.view.convert(point, to: self.listView.view) + var selectedItemNode: ASDisplayNode? + self.listView.forEachItemNode { itemNode in + if itemNode.frame.contains(adjustedPoint) { + selectedItemNode = itemNode + } + } + if let selectedItemNode = selectedItemNode as? HorizontalPeerItemNode, let peer = selectedItemNode.peer { + return (selectedItemNode.view, peer.id) + } + return nil + } } diff --git a/TelegramUI/ChatMessageActionButtonsNode.swift b/TelegramUI/ChatMessageActionButtonsNode.swift index ce8691168e..ab76fb0b5e 100644 --- a/TelegramUI/ChatMessageActionButtonsNode.swift +++ b/TelegramUI/ChatMessageActionButtonsNode.swift @@ -62,7 +62,7 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { return { button, constrainedWidth, position in let sideInset: CGFloat = 8.0 let minimumSideInset: CGFloat = 4.0 - let (titleSize, titleApply) = titleLayout(NSAttributedString(string: button.title, font: titleFont, textColor: .white), nil, 1, .end, CGSize(width: max(1.0, constrainedWidth - minimumSideInset - minimumSideInset), height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (titleSize, titleApply) = titleLayout(NSAttributedString(string: button.title, font: titleFont, textColor: .white), nil, 1, .end, CGSize(width: max(1.0, constrainedWidth - minimumSideInset - minimumSideInset), height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let backgroundImage: UIImage switch position { diff --git a/TelegramUI/ChatMessageActionItemNode.swift b/TelegramUI/ChatMessageActionItemNode.swift index 7249014e86..18e5839eb3 100644 --- a/TelegramUI/ChatMessageActionItemNode.swift +++ b/TelegramUI/ChatMessageActionItemNode.swift @@ -169,7 +169,7 @@ class ChatMessageActionItemNode: ChatMessageItemView { } } - let (size, apply) = labelLayout(attributedString, nil, 1, .end, CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (size, apply) = labelLayout(attributedString, nil, 1, .end, CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let backgroundSize = CGSize(width: size.size.width + 8.0 + 8.0, height: 20.0) var layoutInsets = UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0) diff --git a/TelegramUI/ChatMessageBubbleItemNode.swift b/TelegramUI/ChatMessageBubbleItemNode.swift index b80325d3e1..5c8fd6d230 100644 --- a/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/TelegramUI/ChatMessageBubbleItemNode.swift @@ -316,7 +316,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { attributedString = NSAttributedString(string: "", font: nameFont, textColor: UIColor.black) } - let sizeAndApply = authorNameLayout(attributedString, nil, 1, .end, CGSize(width: maximumNodeWidth, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let sizeAndApply = authorNameLayout(attributedString, nil, 1, .end, CGSize(width: maximumNodeWidth, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) nameNodeSizeApply = (sizeAndApply.0.size, { return sizeAndApply.1() }) diff --git a/TelegramUI/ChatMessageDateAndStatusNode.swift b/TelegramUI/ChatMessageDateAndStatusNode.swift index d174d320bd..78b3c16af9 100644 --- a/TelegramUI/ChatMessageDateAndStatusNode.swift +++ b/TelegramUI/ChatMessageDateAndStatusNode.swift @@ -192,7 +192,7 @@ class ChatMessageDateAndStatusNode: ASTransformLayerNode { updatedDateText = "edited " + updatedDateText } - let (date, dateApply) = dateLayout(NSAttributedString(string: updatedDateText, font: dateFont, textColor: dateColor), nil, 1, .end, constrainedSize, .natural, nil) + let (date, dateApply) = dateLayout(NSAttributedString(string: updatedDateText, font: dateFont, textColor: dateColor), nil, 1, .end, constrainedSize, .natural, nil, UIEdgeInsets()) let statusWidth: CGFloat diff --git a/TelegramUI/ChatMessageDateHeader.swift b/TelegramUI/ChatMessageDateHeader.swift index 4cfebac37a..dfbf7975c8 100644 --- a/TelegramUI/ChatMessageDateHeader.swift +++ b/TelegramUI/ChatMessageDateHeader.swift @@ -2,16 +2,15 @@ import Foundation import Display import AsyncDisplayKit -private let timezoneOffset: Int = { +private let timezoneOffset: Int32 = { let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) var now: time_t = time_t(nowTimestamp) var timeinfoNow: tm = tm() localtime_r(&now, &timeinfoNow) - return Int(timeinfoNow.tm_gmtoff) + return Int32(timeinfoNow.tm_gmtoff) }() private let granularity: Int32 = 60 * 60 * 24 -//private let granularity: Int32 = 60 * 60 final class ChatMessageDateHeader: ListViewItemHeader { private let timestamp: Int32 @@ -122,7 +121,7 @@ final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { let attributedString = NSAttributedString(string: text, font: titleFont, textColor: UIColor.white) let labelLayout = TextNode.asyncLayout(self.labelNode) - let (size, apply) = labelLayout(attributedString, nil, 1, .end, CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (size, apply) = labelLayout(attributedString, nil, 1, .end, CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) apply() self.labelNode.frame = CGRect(origin: CGPoint(), size: size.size) } diff --git a/TelegramUI/ChatMessageFileBubbleContentNode.swift b/TelegramUI/ChatMessageFileBubbleContentNode.swift index 6d4bf5cd48..037079c7cf 100644 --- a/TelegramUI/ChatMessageFileBubbleContentNode.swift +++ b/TelegramUI/ChatMessageFileBubbleContentNode.swift @@ -59,7 +59,12 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode { statusType = nil } - let (initialWidth, refineLayout) = interactiveFileLayout(item.account, item.message, selectedFile!, item.message.effectivelyIncoming, statusType, CGSize(width: constrainedSize.width, height: constrainedSize.height)) + var automaticDownload = false + if selectedFile!.isVoice { + automaticDownload = true + } + + let (initialWidth, refineLayout) = interactiveFileLayout(item.account, item.message, selectedFile!, automaticDownload, item.message.effectivelyIncoming, statusType, CGSize(width: constrainedSize.width, height: constrainedSize.height)) return (initialWidth + layoutConstants.file.bubbleInsets.left + layoutConstants.file.bubbleInsets.right, { constrainedSize in let (refinedWidth, finishLayout) = refineLayout(constrainedSize) diff --git a/TelegramUI/ChatMessageForwardInfoNode.swift b/TelegramUI/ChatMessageForwardInfoNode.swift index 718db12d34..9b2782e2bb 100644 --- a/TelegramUI/ChatMessageForwardInfoNode.swift +++ b/TelegramUI/ChatMessageForwardInfoNode.swift @@ -28,7 +28,7 @@ class ChatMessageForwardInfoNode: ASTransformLayerNode { let color = incoming ? UIColor(0x007bff) : UIColor(0x00a516) let string = NSMutableAttributedString(string: completeString as String, attributes: [NSForegroundColorAttributeName: color, NSFontAttributeName: prefixFont]) string.addAttributes([NSFontAttributeName: peerFont], range: NSMakeRange(prefix.length, completeString.length - prefix.length)) - let (textLayout, textApply) = textNodeLayout(string, nil, 2, .end, constrainedSize, .natural, nil) + let (textLayout, textApply) = textNodeLayout(string, nil, 2, .end, constrainedSize, .natural, nil, UIEdgeInsets()) return (textLayout.size, { let node: ChatMessageForwardInfoNode diff --git a/TelegramUI/ChatMessageInteractiveFileNode.swift b/TelegramUI/ChatMessageInteractiveFileNode.swift index 0dc98731a3..e62c275edc 100644 --- a/TelegramUI/ChatMessageInteractiveFileNode.swift +++ b/TelegramUI/ChatMessageInteractiveFileNode.swift @@ -21,6 +21,9 @@ private let outgoingDescriptionColor = UIColor(0x6fb26a) private let incomingDurationColor = UIColor(0x525252, 0.6) private let outgoingDurationColor = UIColor(0x008c09, 0.8) +private let consumableContentIncomingIcon = generateFilledCircleImage(diameter: 4.0, color: UIColor(0x1581e2)) +private let consumableContentOutgoingIcon = generateFilledCircleImage(diameter: 4.0, color: UIColor(0x19c700)) + private let fileIconIncomingImage = UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocumentIncoming")?.precomposed() private let fileIconOutgoingImage = UIImage(bundleImageName: "Chat/Message/RadialProgressIconDocumentOutgoing")?.precomposed() @@ -29,6 +32,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { private let descriptionNode: TextNode private let waveformNode: AudioWaveformNode private let dateAndStatusNode: ChatMessageDateAndStatusNode + private let consumableContentNode: ASImageNode private var iconNode: TransformImageNode? private var progressNode: RadialProgressNode? @@ -58,6 +62,8 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { self.dateAndStatusNode = ChatMessageDateAndStatusNode() + self.consumableContentNode = ASImageNode() + super.init(layerBacked: false) self.addSubnode(self.titleNode) @@ -111,7 +117,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { } } - func asyncLayout() -> (_ account: Account, _ message: Message, _ file: TelegramMediaFile, _ incoming: Bool, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) { + func asyncLayout() -> (_ account: Account, _ message: Message, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) { let currentFile = self.file let titleAsyncLayout = TextNode.asyncLayout(self.titleNode) @@ -119,7 +125,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { let currentMessage = self.message let statusLayout = self.dateAndStatusNode.asyncLayout() - return { account, message, file, incoming, dateAndStatusType, constrainedSize in + return { account, message, file, automaticDownload, incoming, dateAndStatusType, constrainedSize in return (CGFloat.greatestFiniteMagnitude, { constrainedSize in //var updateImageSignal: Signal DrawingContext, NoError>? var updatedStatusSignal: Signal? @@ -154,6 +160,20 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { var statusSize: CGSize? var statusApply: ((Bool) -> Void)? + var consumableContentIcon: UIImage? + for attribute in message.attributes { + if let attribute = attribute as? ConsumableContentMessageAttribute { + if !attribute.consumed { + if incoming { + consumableContentIcon = consumableContentIncomingIcon + } else { + consumableContentIcon = consumableContentOutgoingIcon + } + } + break + } + } + if let statusType = dateAndStatusType { var t = Int(message.timestamp) var timeinfo = tm() @@ -251,10 +271,9 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { let textConstrainedSize = CGSize(width: constrainedSize.width - 44.0 - 8.0, height: constrainedSize.height) - let (titleLayout, titleApply) = titleAsyncLayout(titleString, nil, 1, .middle, textConstrainedSize, .natural, nil) - let (descriptionLayout, descriptionApply) = descriptionAsyncLayout(descriptionString, nil, 1, .middle, textConstrainedSize, .natural, nil) + let (titleLayout, titleApply) = titleAsyncLayout(titleString, nil, 1, .middle, textConstrainedSize, .natural, nil, UIEdgeInsets()) + let (descriptionLayout, descriptionApply) = descriptionAsyncLayout(descriptionString, nil, 1, .middle, textConstrainedSize, .natural, nil, UIEdgeInsets()) - var voiceWidth: CGFloat = 0.0 let minVoiceWidth: CGFloat = 120.0 let maxVoiceWidth = constrainedSize.width let maxVoiceLength: CGFloat = 30.0 @@ -309,12 +328,24 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { strongSelf.titleNode.frame = titleFrame strongSelf.descriptionNode.frame = descriptionFrame + if let consumableContentIcon = consumableContentIcon { + if strongSelf.consumableContentNode.supernode == nil { + strongSelf.addSubnode(strongSelf.consumableContentNode) + } + if strongSelf.consumableContentNode.image !== consumableContentIcon { + strongSelf.consumableContentNode.image = consumableContentIcon + } + strongSelf.consumableContentNode.frame = CGRect(origin: CGPoint(x: descriptionFrame.maxX + 2.0, y: descriptionFrame.minY + 5.0), size: consumableContentIcon.size) + } else if strongSelf.consumableContentNode.supernode != nil { + strongSelf.consumableContentNode.removeFromSupernode() + } + if let statusApply = statusApply, let statusSize = statusSize { if strongSelf.dateAndStatusNode.supernode == nil { strongSelf.addSubnode(strongSelf.dateAndStatusNode) } - strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: fittedLayoutSize.width - statusSize.width, y: fittedLayoutSize.height - statusSize.height + 10.0), size: statusSize) + strongSelf.dateAndStatusNode.frame = CGRect(origin: CGPoint(x: boundingWidth - statusSize.width, y: fittedLayoutSize.height - statusSize.height + 10.0), size: statusSize) statusApply(false) } else if strongSelf.dateAndStatusNode.supernode != nil { strongSelf.dateAndStatusNode.removeFromSupernode() @@ -324,7 +355,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { if strongSelf.waveformNode.supernode == nil { strongSelf.addSubnode(strongSelf.waveformNode) } - strongSelf.waveformNode.frame = CGRect(origin: CGPoint(x: 43.0, y: -1.0), size: CGSize(width: fittedLayoutSize.width - 41.0, height: 12.0)) + strongSelf.waveformNode.frame = CGRect(origin: CGPoint(x: 43.0, y: -1.0), size: CGSize(width: boundingWidth - 41.0, height: 12.0)) strongSelf.waveformNode.setup(color: UIColor(incoming ? 0x007ee5 : 0x3fc33b), waveform: audioWaveform) } else if strongSelf.waveformNode.supernode != nil { strongSelf.waveformNode.removeFromSupernode() @@ -382,6 +413,9 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { if let updatedFetchControls = updatedFetchControls { let _ = strongSelf.fetchControls.swap(updatedFetchControls) + if automaticDownload { + updatedFetchControls.fetch() + } } } }) @@ -390,12 +424,12 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { } } - static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ account: Account, _ message: Message, _ file: TelegramMediaFile, _ incoming: Bool, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveFileNode))) { + static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ account: Account, _ message: Message, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveFileNode))) { let currentAsyncLayout = node?.asyncLayout() - return { account, message, file, incoming, dateAndStatusType, constrainedSize in + return { account, message, file, automaticDownload, incoming, dateAndStatusType, constrainedSize in var fileNode: ChatMessageInteractiveFileNode - var fileLayout: (_ account: Account, _ message: Message, _ file: TelegramMediaFile, _ incoming: Bool, _ dateAnsStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) + var fileLayout: (_ account: Account, _ message: Message, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ dateAnsStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) if let node = node, let currentAsyncLayout = currentAsyncLayout { fileNode = node @@ -405,7 +439,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { fileLayout = fileNode.asyncLayout() } - let (initialWidth, continueLayout) = fileLayout(account, message, file, incoming, dateAndStatusType, constrainedSize) + let (initialWidth, continueLayout) = fileLayout(account, message, file, automaticDownload, incoming, dateAndStatusType, constrainedSize) return (initialWidth, { constrainedSize in let (finalWidth, finalLayout) = continueLayout(constrainedSize) diff --git a/TelegramUI/ChatMessageReplyInfoNode.swift b/TelegramUI/ChatMessageReplyInfoNode.swift index b0b1c5b180..3492e82abb 100644 --- a/TelegramUI/ChatMessageReplyInfoNode.swift +++ b/TelegramUI/ChatMessageReplyInfoNode.swift @@ -174,8 +174,8 @@ class ChatMessageReplyInfoNode: ASTransformLayerNode { let contrainedTextSize = CGSize(width: maximumTextWidth, height: constrainedSize.height) - let (titleLayout, titleApply) = titleNodeLayout(NSAttributedString(string: titleString, font: titleFont, textColor: titleColor), nil, 1, .end, contrainedTextSize, .natural, nil) - let (textLayout, textApply) = textNodeLayout(NSAttributedString(string: textString, font: textFont, textColor: textMedia ? titleColor : textColor), nil, 1, .end, contrainedTextSize, .natural, nil) + let (titleLayout, titleApply) = titleNodeLayout(NSAttributedString(string: titleString, font: titleFont, textColor: titleColor), nil, 1, .end, contrainedTextSize, .natural, nil, UIEdgeInsets()) + let (textLayout, textApply) = textNodeLayout(NSAttributedString(string: textString, font: textFont, textColor: textMedia ? titleColor : textColor), nil, 1, .end, contrainedTextSize, .natural, nil, UIEdgeInsets()) let size = CGSize(width: max(titleLayout.size.width, textLayout.size.width) + leftInset, height: titleLayout.size.height + textLayout.size.height) diff --git a/TelegramUI/ChatMessageTextBubbleContentNode.swift b/TelegramUI/ChatMessageTextBubbleContentNode.swift index f8f6e8da45..a99ec732bf 100644 --- a/TelegramUI/ChatMessageTextBubbleContentNode.swift +++ b/TelegramUI/ChatMessageTextBubbleContentNode.swift @@ -118,7 +118,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { attributedText = NSAttributedString(string: message.text, font: messageFont, textColor: UIColor.black) } - let (textLayout, textApply) = textLayout(attributedText, nil, 0, .end, textConstrainedSize, .natural, nil) + let (textLayout, textApply) = textLayout(attributedText, nil, 0, .end, textConstrainedSize, .natural, nil, UIEdgeInsets()) var textFrame = CGRect(origin: CGPoint(), size: textLayout.size) let textSize = textLayout.size diff --git a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift index 7922f322bf..fd11efa0d6 100644 --- a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift +++ b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift @@ -152,7 +152,11 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { initialWidth = initialImageWidth + insets.left + insets.right refineContentImageLayout = refineLayout } else { - let (_, refineLayout) = contentFileLayout(item.account, item.message, file, item.message.effectivelyIncoming, nil, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height)) + var automaticDownload = false + if file.isVoice { + automaticDownload = true + } + let (_, refineLayout) = contentFileLayout(item.account, item.message, file, automaticDownload, item.message.effectivelyIncoming, nil, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height)) refineContentFileLayout = refineLayout } } else if let image = webpage.image { @@ -200,7 +204,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { statusSizeAndApply = statusLayout(edited && !sentViaBot, viewCount, dateText, statusType, textConstrainedSize) } - let (textLayout, textApply) = textAsyncLayout(textString, nil, 12, .end, textConstrainedSize, .natural, textCutout) + let (textLayout, textApply) = textAsyncLayout(textString, nil, 12, .end, textConstrainedSize, .natural, textCutout, UIEdgeInsets()) var textFrame = CGRect(origin: CGPoint(), size: textLayout.size) diff --git a/TelegramUI/ChatPinnedMessageTitlePanelNode.swift b/TelegramUI/ChatPinnedMessageTitlePanelNode.swift index 4be8a2ba2d..83b87c64e1 100644 --- a/TelegramUI/ChatPinnedMessageTitlePanelNode.swift +++ b/TelegramUI/ChatPinnedMessageTitlePanelNode.swift @@ -154,9 +154,9 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { let rightInset: CGFloat = 18.0 let textRightInset: CGFloat = 25.0 - let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: "Pinned message", font: Font.medium(15.0), textColor: UIColor(0x007ee5)), nil, 1, .end, CGSize(width: width - leftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: "Pinned message", font: Font.medium(15.0), textColor: UIColor(0x007ee5)), nil, 1, .end, CGSize(width: width - leftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) - let (textLayout, textApply) = makeTextLayout(NSAttributedString(string: message.text, font: Font.regular(15.0), textColor: .black), nil, 1, .end, CGSize(width: width - leftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (textLayout, textApply) = makeTextLayout(NSAttributedString(string: message.text, font: Font.regular(15.0), textColor: .black), nil, 1, .end, CGSize(width: width - leftInset - rightInset - textRightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) Queue.mainQueue().async { if let strongSelf = self { diff --git a/TelegramUI/ChatTextInputAudioRecordingCancelIndicator.swift b/TelegramUI/ChatTextInputAudioRecordingCancelIndicator.swift index 2225426c9d..06c01b1219 100644 --- a/TelegramUI/ChatTextInputAudioRecordingCancelIndicator.swift +++ b/TelegramUI/ChatTextInputAudioRecordingCancelIndicator.swift @@ -24,7 +24,7 @@ final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode { self.addSubnode(self.labelNode) let makeLayout = TextNode.asyncLayout(self.labelNode) - let (labelLayout, labelApply) = makeLayout(NSAttributedString(string: "Slide to cancel", font: Font.regular(14.0), textColor: UIColor(0xaaaab2)), nil, 1, .end, CGSize(width: 200.0, height: 100.0), .natural, nil) + let (labelLayout, labelApply) = makeLayout(NSAttributedString(string: "Slide to cancel", font: Font.regular(14.0), textColor: UIColor(0xaaaab2)), nil, 1, .end, CGSize(width: 200.0, height: 100.0), .natural, nil, UIEdgeInsets()) labelApply() let arrowSize = arrowImage?.size ?? CGSize() diff --git a/TelegramUI/ChatTextInputAudioRecordingTimeNode.swift b/TelegramUI/ChatTextInputAudioRecordingTimeNode.swift index dd16a9a22a..cf6f4ecdb9 100644 --- a/TelegramUI/ChatTextInputAudioRecordingTimeNode.swift +++ b/TelegramUI/ChatTextInputAudioRecordingTimeNode.swift @@ -57,7 +57,7 @@ final class ChatTextInputAudioRecordingTimeNode: ASDisplayNode { override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { let makeLayout = TextNode.asyncLayout(self.textNode) - let (size, apply) = makeLayout(NSAttributedString(string: "00:00,00", font: Font.regular(15.0), textColor: .black), nil, 1, .end, CGSize(width: 200.0, height: 100.0), .natural, nil) + let (size, apply) = makeLayout(NSAttributedString(string: "00:00,00", font: Font.regular(15.0), textColor: .black), nil, 1, .end, CGSize(width: 200.0, height: 100.0), .natural, nil, UIEdgeInsets()) apply() self.textNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 1.0 + UIScreenPixel), size: size.size) return size.size diff --git a/TelegramUI/ChatTextInputPanelNode.swift b/TelegramUI/ChatTextInputPanelNode.swift index 5948b9330e..7fae97badc 100644 --- a/TelegramUI/ChatTextInputPanelNode.swift +++ b/TelegramUI/ChatTextInputPanelNode.swift @@ -334,7 +334,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.view.addSubview(self.textInputBackgroundView) let placeholderLayout = TextNode.asyncLayout(self.textPlaceholderNode) - let (placeholderSize, placeholderApply) = placeholderLayout(NSAttributedString(string: "Message", font: Font.regular(17.0), textColor: UIColor(0xC8C8CE)), nil, 1, .end, CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (placeholderSize, placeholderApply) = placeholderLayout(NSAttributedString(string: "Message", font: Font.regular(17.0), textColor: UIColor(0xC8C8CE)), nil, 1, .end, CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) self.textPlaceholderNode.frame = CGRect(origin: CGPoint(), size: placeholderSize.size) let _ = placeholderApply() self.addSubnode(self.textPlaceholderNode) @@ -424,7 +424,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { if self.currentPlaceholder != placeholder { self.currentPlaceholder = placeholder let placeholderLayout = TextNode.asyncLayout(self.textPlaceholderNode) - let (placeholderSize, placeholderApply) = placeholderLayout(NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: UIColor(0xbebec0)), nil, 1, .end, CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (placeholderSize, placeholderApply) = placeholderLayout(NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: UIColor(0xbebec0)), nil, 1, .end, CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) self.textPlaceholderNode.frame = CGRect(origin: self.textPlaceholderNode.frame.origin, size: placeholderSize.size) let _ = placeholderApply() } @@ -641,7 +641,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { if let contextPlaceholder = self.presentationInterfaceState.inputTextPanelState.contextPlaceholder { let placeholderLayout = TextNode.asyncLayout(self.contextPlaceholderNode) - let (placeholderSize, placeholderApply) = placeholderLayout(contextPlaceholder, nil, 1, .end, CGSize(width: width - self.textFieldInsets.left - self.textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (placeholderSize, placeholderApply) = placeholderLayout(contextPlaceholder, nil, 1, .end, CGSize(width: width - self.textFieldInsets.left - self.textFieldInsets.right - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right - accessoryButtonsWidth, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let contextPlaceholderNode = placeholderApply() if let currentContextPlaceholderNode = self.contextPlaceholderNode, currentContextPlaceholderNode !== contextPlaceholderNode { self.contextPlaceholderNode = nil diff --git a/TelegramUI/ChatUnreadItem.swift b/TelegramUI/ChatUnreadItem.swift index 33c5c4cee1..c618f4dc04 100644 --- a/TelegramUI/ChatUnreadItem.swift +++ b/TelegramUI/ChatUnreadItem.swift @@ -100,7 +100,7 @@ class ChatUnreadItemNode: ListViewItemNode { let labelLayout = TextNode.asyncLayout(self.labelNode) let layoutConstants = self.layoutConstants return { item, width, dateAtBottom in - let (size, apply) = labelLayout(NSAttributedString(string: "Unread", font: titleFont, textColor: UIColor(0x86868d)), nil, 1, .end, CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (size, apply) = labelLayout(NSAttributedString(string: "Unread", font: titleFont, textColor: UIColor(0x86868d)), nil, 1, .end, CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let backgroundSize = CGSize(width: width, height: 25.0) diff --git a/TelegramUI/ChatVideoGalleryItem.swift b/TelegramUI/ChatVideoGalleryItem.swift index df105ca83f..f695504f8d 100644 --- a/TelegramUI/ChatVideoGalleryItem.swift +++ b/TelegramUI/ChatVideoGalleryItem.swift @@ -55,11 +55,16 @@ final class ChatVideoGalleryItemNode: ZoomableContentGalleryItemNode { private let videoNode: MediaPlayerNode private let scrubberView: ChatVideoGalleryItemScrubberView + private let progressButtonNode: HighlightableButtonNode + private let progressNode: RadialProgressNode + private var accountAndFile: (Account, TelegramMediaFile, Bool)? private var isCentral = false - private let videoStatusDisposable = MetaDisposable() + private let fetchStatusDisposable = MetaDisposable() + private let fetchDisposable = MetaDisposable() + private var resourceStatus: MediaResourceStatus? override init() { self.videoNode = MediaPlayerNode() @@ -68,6 +73,9 @@ final class ChatVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.videoNode.snapshotNode = snapshotNode self.scrubberView = ChatVideoGalleryItemScrubberView() + self.progressButtonNode = HighlightableButtonNode() + self.progressNode = RadialProgressNode(theme: RadialProgressTheme(backgroundColor: UIColor(white: 0.0, alpha: 0.6), foregroundColor: UIColor.white, icon: nil)) + super.init() self.snapshotNode.imageUpdated = { [weak self] in @@ -78,10 +86,14 @@ final class ChatVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.scrubberView.seek = { [weak self] timestamp in self?.player?.seek(timestamp: timestamp) } + + self.progressButtonNode.addSubnode(self.progressNode) + self.progressButtonNode.addTarget(self, action: #selector(progressButtonPressed), forControlEvents: .touchUpInside) } deinit { - self.videoStatusDisposable.dispose() + self.fetchStatusDisposable.dispose() + self.fetchDisposable.dispose() } override func ready() -> Signal { @@ -90,6 +102,11 @@ final class ChatVideoGalleryItemNode: ZoomableContentGalleryItemNode { override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + + let progressDiameter: CGFloat = 50.0 + let progressFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - progressDiameter) / 2.0), y: floor((layout.size.height - progressDiameter) / 2.0)), size: CGSize(width: progressDiameter, height: progressDiameter)) + transition.updateFrame(node: self.progressButtonNode, frame: progressFrame) + transition.updateFrame(node: self.progressNode, frame: CGRect(origin: CGPoint(), size: progressFrame.size)) } func setFile(account: Account, file: TelegramMediaFile, loopVideo: Bool) { @@ -104,10 +121,32 @@ final class ChatVideoGalleryItemNode: ZoomableContentGalleryItemNode { self._ready.set(.single(Void())) } + self.resourceStatus = nil + self.fetchStatusDisposable.set((account.postbox.mediaBox.resourceStatus(file.resource) |> deliverOnMainQueue).start(next: { [weak self] status in + if let strongSelf = self { + strongSelf.resourceStatus = status + switch status { + case let .Fetching(progress): + strongSelf.progressNode.state = .Fetching(progress: progress) + strongSelf.progressButtonNode.isHidden = false + case .Local: + strongSelf.progressNode.state = .Play + strongSelf.progressButtonNode.isHidden = strongSelf.player != nil + case .Remote: + strongSelf.progressNode.state = .Remote + strongSelf.progressButtonNode.isHidden = false + } + } + })) + if self.progressButtonNode.supernode == nil { + self.addSubnode(self.progressButtonNode) + } + let shouldPlayVideo = self.accountAndFile?.1 != file self.accountAndFile = (account, file, loopVideo) if shouldPlayVideo && self.isCentral { - self.playVideo() + self.progressButtonPressed() + //self.playVideo() } } } @@ -126,17 +165,14 @@ final class ChatVideoGalleryItemNode: ZoomableContentGalleryItemNode { /*let source = VideoPlayerSource(account: account, resource: CloudFileMediaResource(location: file.location, size: file.size)) self.videoNode.player = VideoPlayer(source: source)*/ - let player = MediaPlayer(postbox: account.postbox, resource: file.resource) + let player = MediaPlayer(audioSessionManager: (account.applicationContext as! TelegramApplicationContext).mediaManager.audioSession, postbox: account.postbox, resource: file.resource, streamable: false) if loopVideo { player.actionAtEnd = .loop } player.attachPlayerNode(self.videoNode) + self.progressButtonNode.isHidden = true self.player = player - self.videoStatusDisposable.set((player.status |> deliverOnMainQueue).start(next: { [weak self] status in - if let strongSelf = self { - strongSelf.scrubberView.setStatus(status) - } - })) + self.scrubberView.setStatusSignal(player.status) player.play() self.zoomableContent = (dimensions, self.videoNode) @@ -146,6 +182,7 @@ final class ChatVideoGalleryItemNode: ZoomableContentGalleryItemNode { private func stopVideo() { self.player = nil + self.progressButtonNode.isHidden = false } override func centralityUpdated(isCentral: Bool) { @@ -211,6 +248,13 @@ final class ChatVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + self.progressNode.layer.animatePosition(from: self.progressNode.layer.position, to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + //positionCompleted = true + //intermediateCompletion() + }) + self.progressNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + self.progressNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.25, removeOnCompletion: false) + self.videoNode.snapshotNode?.isHidden = true transformedFrame.origin = CGPoint() @@ -230,4 +274,34 @@ final class ChatVideoGalleryItemNode: ZoomableContentGalleryItemNode { override func titleView() -> Signal { return self._titleView.get() } + + private func activateVideo() { + if let (account, file, _) = self.accountAndFile { + if let resourceStatus = self.resourceStatus { + switch resourceStatus { + case .Fetching: + break + case .Local: + self.playVideo() + case .Remote: + self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(file.resource).start()) + } + } + } + } + + @objc func progressButtonPressed() { + if let (account, file, _) = self.accountAndFile { + if let resourceStatus = self.resourceStatus { + switch resourceStatus { + case .Fetching: + account.postbox.mediaBox.cancelInteractiveResourceFetch(file.resource) + case .Local: + self.playVideo() + case .Remote: + self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(file.resource).start()) + } + } + } + } } diff --git a/TelegramUI/ChatVideoGalleryItemScrubberView.swift b/TelegramUI/ChatVideoGalleryItemScrubberView.swift index 6b142e73ef..bb26c89f21 100644 --- a/TelegramUI/ChatVideoGalleryItemScrubberView.swift +++ b/TelegramUI/ChatVideoGalleryItemScrubberView.swift @@ -1,93 +1,53 @@ import Foundation import UIKit +import SwiftSignalKit final class ChatVideoGalleryItemScrubberView: UIView { - private let backgroundView: UIView - private let foregroundView: UIView - private let handleView: UIView - - private var status: MediaPlayerStatus? - - private var scrubbing = false - private var scrubbingLocation: CGFloat = 0.0 - private var initialScrubbingPosition: CGFloat = 0.0 - private var scrubbingPosition: CGFloat = 0.0 + private let leftTimestampNode: MediaPlayerTimeTextNode + private let rightTimestampNode: MediaPlayerTimeTextNode + private let scrubberNode: MediaPlayerScrubbingNode var seek: (Double) -> Void = { _ in } override init(frame: CGRect) { - self.backgroundView = UIView() - self.backgroundView.backgroundColor = UIColor.gray - self.backgroundView.clipsToBounds = true - self.foregroundView = UIView() - self.foregroundView.backgroundColor = UIColor.white - self.handleView = UIView() - self.handleView.backgroundColor = UIColor.white + self.scrubberNode = MediaPlayerScrubbingNode(lineHeight: 3.0, lineCap: .round, scrubberHandle: true, backgroundColor: .gray, foregroundColor: .white) + + self.leftTimestampNode = MediaPlayerTimeTextNode(textColor: .white) + self.rightTimestampNode = MediaPlayerTimeTextNode(textColor: .white) + self.leftTimestampNode.alignment = .right + self.rightTimestampNode.mode = .reversed super.init(frame: frame) - self.backgroundView.addSubview(self.foregroundView) - self.addSubview(self.backgroundView) - self.addSubview(self.handleView) + self.scrubberNode.seek = { [weak self] timestamp in + self?.seek(timestamp) + } - self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))) + self.addSubnode(self.scrubberNode) + self.addSubnode(self.leftTimestampNode) + self.addSubnode(self.rightTimestampNode) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - func setStatus(_ status: MediaPlayerStatus) { - self.status = status - self.layoutSubviews() - - if status.status == .playing { - - } - } - - @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { - guard let status = self.status, status.duration > 0.0 else { - return - } - - switch recognizer.state { - case .began: - self.scrubbing = true - self.scrubbingLocation = recognizer.location(in: self).x - self.initialScrubbingPosition = CGFloat(status.timestamp / status.duration) - self.scrubbingPosition = 0.0 - case .changed: - let distance = recognizer.location(in: self).x - self.scrubbingLocation - self.scrubbingPosition = self.initialScrubbingPosition + (distance / self.bounds.size.width) - self.layoutSubviews() - case .ended: - self.scrubbing = false - self.seek(Double(self.scrubbingPosition) * status.duration) - default: - break - } + func setStatusSignal(_ status: Signal?) { + self.scrubberNode.status = status + self.leftTimestampNode.status = status + self.rightTimestampNode.status = status } override func layoutSubviews() { super.layoutSubviews() let size = self.bounds.size - let barHeight: CGFloat = 2.0 - let handleHeight: CGFloat = 14.0 - self.backgroundView.frame = CGRect(origin: CGPoint(x: 0.0, y: floor(size.height - barHeight) / 2.0), size: CGSize(width: size.width, height: barHeight)) + let scrubberHeight: CGFloat = 14.0 - var position: CGFloat = 0.0 - if self.scrubbing { - position = self.scrubbingPosition - } else { - if let status = self.status, status.duration > 0.0 { - position = CGFloat(status.timestamp / status.duration) - } - } + self.leftTimestampNode.frame = CGRect(origin: CGPoint(x: -10.0, y: 15.0), size: CGSize(width: 57.0 - 15.0, height: 20.0)) + self.rightTimestampNode.frame = CGRect(origin: CGPoint(x: size.width - 57.0 + 30.0, y: 15.0), size: CGSize(width: 57.0 - 10.0, height: 20.0)) - self.foregroundView.frame = CGRect(origin: CGPoint(x: -size.width + floor(position * size.width), y: 0.0), size: CGSize(width: size.width, height: barHeight)) - self.handleView.frame = CGRect(origin: CGPoint(x: floor(position * size.width), y: floor(size.height - handleHeight) / 2.0), size: CGSize(width: 1.5, height: handleHeight)) + self.scrubberNode.frame = CGRect(origin: CGPoint(x: 57.0 - 15.0, y: floor((size.height - scrubberHeight) / 2.0) + 1.0), size: CGSize(width: size.width - 57.0 * 2.0 + 35.0, height: scrubberHeight)) } } diff --git a/TelegramUI/CommandChatInputPanelItem.swift b/TelegramUI/CommandChatInputPanelItem.swift index ad678d6a51..dbbffc9dc6 100644 --- a/TelegramUI/CommandChatInputPanelItem.swift +++ b/TelegramUI/CommandChatInputPanelItem.swift @@ -164,7 +164,7 @@ final class CommandChatInputPanelItemNode: ListViewItemNode { commandString.append(NSAttributedString(string: " " + item.command.command.description, font: descriptionFont, textColor: descriptionColor)) } - let (textLayout, textApply) = makeTextLayout(commandString, nil, 1, .end, CGSize(width: width - leftInset - rightInset, height: 100.0), .natural, nil) + let (textLayout, textApply) = makeTextLayout(commandString, nil, 1, .end, CGSize(width: width - leftInset - rightInset, height: 100.0), .natural, nil, UIEdgeInsets()) let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: HashtagChatInputPanelItemNode.itemHeight), insets: UIEdgeInsets()) diff --git a/TelegramUI/ContactListActionItem.swift b/TelegramUI/ContactListActionItem.swift index d6c6e1d6ae..6788a4a017 100644 --- a/TelegramUI/ContactListActionItem.swift +++ b/TelegramUI/ContactListActionItem.swift @@ -103,7 +103,7 @@ class ContactListActionItemNode: ListViewItemNode { return { item, width in let leftInset: CGFloat = 65.0 - let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: UIColor(0x007ee5)), nil, 1, .end, CGSize(width: width - 10.0 - leftInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: UIColor(0x007ee5)), nil, 1, .end, CGSize(width: width - 10.0 - leftInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let contentSize = CGSize(width: width, height: 48.0) let insets = UIEdgeInsets() diff --git a/TelegramUI/ContactsPeerItem.swift b/TelegramUI/ContactsPeerItem.swift index efb3f13868..33a4bb126b 100644 --- a/TelegramUI/ContactsPeerItem.swift +++ b/TelegramUI/ContactsPeerItem.swift @@ -178,6 +178,9 @@ class ContactsPeerItemNode: ListViewItemNode { private var peerPresenceManager: PeerPresenceStatusManager? private var layoutParams: (ContactsPeerItem, CGFloat, Bool, Bool, Bool)? + var peer: Peer? { + return self.layoutParams?.0.peer + } required init() { self.backgroundNode = ASDisplayNode() @@ -323,9 +326,9 @@ class ContactsPeerItemNode: ListViewItemNode { } } - let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: max(0.0, width - leftInset - rightInset), height: CGFloat.infinity), .natural, nil) + let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: max(0.0, width - leftInset - rightInset), height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) - let (statusLayout, statusApply) = makeStatusLayout(statusAttributedString, nil, 1, .end, CGSize(width: max(0.0, width - leftInset - rightInset), height: CGFloat.infinity), .natural, nil) + let (statusLayout, statusApply) = makeStatusLayout(statusAttributedString, nil, 1, .end, CGSize(width: max(0.0, width - leftInset - rightInset), height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 48.0), insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0)) diff --git a/TelegramUI/ContactsVCardItem.swift b/TelegramUI/ContactsVCardItem.swift index c25a74a6de..ea2bcc48e4 100644 --- a/TelegramUI/ContactsVCardItem.swift +++ b/TelegramUI/ContactsVCardItem.swift @@ -172,9 +172,9 @@ class ContactsVCardItemNode: ListViewItemNode { } } - let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: max(0.0, width - leftInset - rightInset), height: CGFloat.infinity), .natural, nil) + let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: max(0.0, width - leftInset - rightInset), height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) - let (statusLayout, statusApply) = makeStatusLayout(statusAttributedString, nil, 1, .end, CGSize(width: max(0.0, width - leftInset - rightInset), height: CGFloat.infinity), .natural, nil) + let (statusLayout, statusApply) = makeStatusLayout(statusAttributedString, nil, 1, .end, CGSize(width: max(0.0, width - leftInset - rightInset), height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 78.0), insets: UIEdgeInsets()) diff --git a/TelegramUI/DeclareEncodables.swift b/TelegramUI/DeclareEncodables.swift index 5d4ce2e5bc..368c76d513 100644 --- a/TelegramUI/DeclareEncodables.swift +++ b/TelegramUI/DeclareEncodables.swift @@ -5,6 +5,7 @@ private var telegramUIDeclaredEncodables: Void = { declareEncodable(ChatEmbeddedInterfaceState.self, f: { ChatEmbeddedInterfaceState(decoder: $0) }) declareEncodable(VideoLibraryMediaResource.self, f: { VideoLibraryMediaResource(decoder: $0) }) declareEncodable(LocalFileVideoMediaResource.self, f: { LocalFileVideoMediaResource(decoder: $0) }) + declareEncodable(PhotoLibraryMediaResource.self, f: { PhotoLibraryMediaResource(decoder: $0) }) return }() diff --git a/TelegramUI/FFMpegMediaFrameSource.swift b/TelegramUI/FFMpegMediaFrameSource.swift index da7423b473..bd98ebb492 100644 --- a/TelegramUI/FFMpegMediaFrameSource.swift +++ b/TelegramUI/FFMpegMediaFrameSource.swift @@ -69,6 +69,7 @@ final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { private let queue: Queue private let postbox: Postbox private let resource: MediaResource + private let streamable: Bool private let taskQueue: ThreadTaskQueue private let thread: Thread @@ -87,10 +88,11 @@ final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { } } - init(queue: Queue, postbox: Postbox, resource: MediaResource) { + init(queue: Queue, postbox: Postbox, resource: MediaResource, streamable: Bool) { self.queue = queue self.postbox = postbox self.resource = resource + self.streamable = streamable self.taskQueue = ThreadTaskQueue() @@ -139,8 +141,9 @@ final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { let postbox = self.postbox let resource = self.resource let queue = self.queue + let streamable = self.streamable self.performWithContext { [weak self] context in - context.initializeState(postbox: postbox, resource: resource) + context.initializeState(postbox: postbox, resource: resource, streamable: streamable) let (frames, endOfStream) = context.takeFrames(until: timestamp) @@ -182,9 +185,10 @@ final class FFMpegMediaFrameSource: NSObject, MediaFrameSource { let queue = self.queue let postbox = self.postbox let resource = self.resource + let streamable = self.streamable self.performWithContext { [weak self] context in - context.initializeState(postbox: postbox, resource: resource) + context.initializeState(postbox: postbox, resource: resource, streamable: streamable) context.seek(timestamp: timestamp, completed: { [weak self] streamDescriptions, timestamp in queue.async { [weak self] in diff --git a/TelegramUI/FFMpegMediaFrameSourceContext.swift b/TelegramUI/FFMpegMediaFrameSourceContext.swift index 42416676df..359702fdd8 100644 --- a/TelegramUI/FFMpegMediaFrameSourceContext.swift +++ b/TelegramUI/FFMpegMediaFrameSourceContext.swift @@ -44,7 +44,7 @@ struct FFMpegMediaFrameSourceContextInfo { private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: UnsafeMutablePointer?, bufferSize: Int32) -> Int32 { let context = Unmanaged.fromOpaque(userData!).takeUnretainedValue() - guard let postbox = context.postbox, let resource = context.resource else { + guard let postbox = context.postbox, let resource = context.resource, let streamable = context.streamable else { return 0 } @@ -55,7 +55,7 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa let readCount = min(resourceSize - context.readingOffset, Int(bufferSize)) var fetchedData: Data? - if resource.streamable { + if streamable { let data: Signal data = postbox.mediaBox.resourceData(resource, size: resourceSize, in: context.readingOffset ..< (context.readingOffset + readCount), mode: .complete) let semaphore = DispatchSemaphore(value: 0) @@ -75,8 +75,8 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa if let data = try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [.mappedIfSafe]) { fetchedData = data.subdata(in: Range(range)) } + semaphore.signal() } - semaphore.signal() }) semaphore.wait() } @@ -93,7 +93,7 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whence: Int32) -> Int64 { let context = Unmanaged.fromOpaque(userData!).takeUnretainedValue() - guard let postbox = context.postbox, let resource = context.resource else { + guard let postbox = context.postbox, let resource = context.resource, let streamable = context.streamable else { return 0 } @@ -113,7 +113,7 @@ private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whe context.fetchedDataDisposable.set(nil) context.requestedCompleteFetch = false } else { - if resource.streamable { + if streamable { context.fetchedDataDisposable.set(postbox.mediaBox.fetchedResourceData(resource, size: resourceSize, in: context.readingOffset ..< resourceSize).start()) } else if !context.requestedCompleteFetch { context.requestedCompleteFetch = true @@ -133,6 +133,7 @@ final class FFMpegMediaFrameSourceContext: NSObject { fileprivate var postbox: Postbox? fileprivate var resource: MediaResource? + fileprivate var streamable: Bool? private let ioBufferSize = 64 * 1024 fileprivate var readingOffset = 0 @@ -156,7 +157,7 @@ final class FFMpegMediaFrameSourceContext: NSObject { fetchedDataDisposable.dispose() } - func initializeState(postbox: Postbox, resource: MediaResource) { + func initializeState(postbox: Postbox, resource: MediaResource, streamable: Bool) { if self.readingError || self.initializedState != nil { return } @@ -165,10 +166,11 @@ final class FFMpegMediaFrameSourceContext: NSObject { self.postbox = postbox self.resource = resource + self.streamable = streamable let resourceSize: Int = resource.size ?? 0 - if resource.streamable { + if streamable { self.fetchedDataDisposable.set(postbox.mediaBox.fetchedResourceData(resource, size: resourceSize, in: 0 ..< resourceSize).start()) } else if !self.requestedCompleteFetch { self.requestedCompleteFetch = true @@ -316,7 +318,7 @@ final class FFMpegMediaFrameSourceContext: NSObject { if let packet = self.readPacket() { if let videoStream = initializedState.videoStream, Int(packet.packet.stream_index) == videoStream.index { let avNoPtsRawValue: UInt64 = 0x8000000000000000 - let avNoPtsValue = unsafeBitCast(avNoPtsRawValue, to: Int64.self) + let avNoPtsValue = Int64(bitPattern: avNoPtsRawValue) let packetPts = packet.packet.pts == avNoPtsValue ? packet.packet.dts : packet.packet.pts let pts = CMTimeMake(packetPts, videoStream.timebase.timescale) @@ -339,7 +341,7 @@ final class FFMpegMediaFrameSourceContext: NSObject { } } else if let audioStream = initializedState.audioStream, Int(packet.packet.stream_index) == audioStream.index { let avNoPtsRawValue: UInt64 = 0x8000000000000000 - let avNoPtsValue = unsafeBitCast(avNoPtsRawValue, to: Int64.self) + let avNoPtsValue = Int64(bitPattern: avNoPtsRawValue) let packetPts = packet.packet.pts == avNoPtsValue ? packet.packet.dts : packet.packet.pts let pts = CMTimeMake(packetPts, audioStream.timebase.timescale) @@ -411,18 +413,21 @@ final class FFMpegMediaFrameSourceContext: NSObject { videoDescription = FFMpegMediaFrameSourceDescription(duration: videoStream.duration, decoder: videoStream.decoder) } - let actualPts: CMTime - if let packet = self.readPacketInternal() { - self.packetQueue.append(packet) - if let videoStream = initializedState.videoStream, Int(packet.packet.stream_index) == videoStream.index { - actualPts = CMTimeMake(packet.pts, videoStream.timebase.timescale) - } else if let audioStream = initializedState.audioStream, Int(packet.packet.stream_index) == audioStream.index { - actualPts = CMTimeMake(packet.pts, audioStream.timebase.timescale) + var actualPts: CMTime = CMTimeMake(0, 1) + for _ in 0 ..< 24 { + if let packet = self.readPacketInternal() { + if let videoStream = initializedState.videoStream, Int(packet.packet.stream_index) == videoStream.index { + self.packetQueue.append(packet) + actualPts = CMTimeMake(packet.pts, videoStream.timebase.timescale) + break + } else if let audioStream = initializedState.audioStream, Int(packet.packet.stream_index) == audioStream.index { + self.packetQueue.append(packet) + actualPts = CMTimeMake(packet.pts, audioStream.timebase.timescale) + break + } } else { - actualPts = CMTimeMake(0, 1) + break } - } else { - actualPts = CMTimeMake(0, 1) } completed(FFMpegMediaFrameSourceDescriptionSet(audio: audioDescription, video: videoDescription), actualPts) diff --git a/TelegramUI/FetchCachedRepresentations.swift b/TelegramUI/FetchCachedRepresentations.swift index 8e36c039de..194d92c41e 100644 --- a/TelegramUI/FetchCachedRepresentations.swift +++ b/TelegramUI/FetchCachedRepresentations.swift @@ -12,6 +12,8 @@ public func fetchCachedResourceRepresentation(account: Account, resource: MediaR return fetchCachedStickerAJpegRepresentation(account: account, resource: resource, resourceData: resourceData, representation: representation) } else if let representation = representation as? CachedScaledImageRepresentation { return fetchCachedScaledImageRepresentation(account: account, resource: resource, resourceData: resourceData, representation: representation) + } else if let representation = representation as? CachedVideoFirstFrameRepresentation { + return fetchCachedVideoFirstFrameRepresentation(account: account, resource: resource, resourceData: resourceData, representation: representation) } return .never() } @@ -126,3 +128,50 @@ private func fetchCachedScaledImageRepresentation(account: Account, resource: Me return EmptyDisposable }) |> runOn(account.graphicsThreadPool) } + +private func fetchCachedVideoFirstFrameRepresentation(account: Account, resource: MediaResource, resourceData: MediaResourceData, representation: CachedVideoFirstFrameRepresentation) -> Signal { + return Signal { subscriber in + if resourceData.complete { + let tempFilePath = NSTemporaryDirectory() + "\(arc4random()).mov" + + _ = try? FileManager.default.removeItem(atPath: tempFilePath) + _ = try? FileManager.default.linkItem(atPath: resourceData.path, toPath: tempFilePath) + + var fullSizeImage: CGImage? + + let asset = AVAsset(url: URL(fileURLWithPath: tempFilePath)) + let imageGenerator = AVAssetImageGenerator(asset: asset) + imageGenerator.maximumSize = CGSize(width: 800.0, height: 800.0) + imageGenerator.appliesPreferredTrackTransform = true + if let image = try? imageGenerator.copyCGImage(at: CMTime(seconds: 0.0, preferredTimescale: asset.duration.timescale), actualTime: nil) { + fullSizeImage = image + } + + if let fullSizeImage = fullSizeImage { + var randomId: Int64 = 0 + arc4random_buf(&randomId, 8) + let path = NSTemporaryDirectory() + "\(randomId)" + let url = URL(fileURLWithPath: path) + + if let colorDestination = CGImageDestinationCreateWithURL(url as CFURL, kUTTypeJPEG, 1, nil) { + CGImageDestinationSetProperties(colorDestination, nil) + + let colorQuality: Float = 0.6 + + let options = NSMutableDictionary() + options.setObject(colorQuality as NSNumber, forKey: kCGImageDestinationLossyCompressionQuality as NSString) + + CGImageDestinationAddImage(colorDestination, fullSizeImage, options as CFDictionary) + if CGImageDestinationFinalize(colorDestination) { + subscriber.putNext(CachedMediaResourceRepresentationResult(temporaryPath: path)) + subscriber.putCompletion() + } + } + + subscriber.putNext(CachedMediaResourceRepresentationResult(temporaryPath: path)) + subscriber.putCompletion() + } + } + return EmptyDisposable + } |> runOn(account.graphicsThreadPool) +} diff --git a/TelegramUI/FetchPhotoLibraryImageResource.swift b/TelegramUI/FetchPhotoLibraryImageResource.swift new file mode 100644 index 0000000000..624a2e50ca --- /dev/null +++ b/TelegramUI/FetchPhotoLibraryImageResource.swift @@ -0,0 +1,66 @@ +import Foundation +import Photos +import Postbox +import SwiftSignalKit + +func fetchPhotoLibraryResource(localIdentifier: String) -> Signal { + return Signal { subscriber in + let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [localIdentifier], options: nil) + var requestId: PHImageRequestID? + if fetchResult.count != 0 { + let asset = fetchResult.object(at: 0) + let option = PHImageRequestOptions() + option.deliveryMode = .opportunistic + option.isNetworkAccessAllowed = true + option.isSynchronous = false + let madeProgress = Atomic(value: false) + option.progressHandler = { progress, error, _, _ in + if !madeProgress.swap(true) { + subscriber.putNext(.reset) + } + } + let size = CGSize(width: 1280.0, height: 1280.0) + requestId = PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFit, options: option, resultHandler: { (image, info) -> Void in + Queue.concurrentDefaultQueue().async { + requestId = nil + if let image = image { + if let info = info, let degraded = info[PHImageResultIsDegradedKey], (degraded as AnyObject).boolValue!{ + if !madeProgress.swap(true) { + subscriber.putNext(.reset) + } + } else { + _ = madeProgress.swap(true) + + let scale = min(1.0, min(size.width / max(1.0, image.size.width), size.height / max(1.0, image.size.height))) + let scaledSize = CGSize(width: floor(image.size.width * scale), height: floor(image.size.height * scale)) + + UIGraphicsBeginImageContextWithOptions(scaledSize, true, image.scale) + image.draw(in: CGRect(origin: CGPoint(), size: scaledSize)) + let scaledImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + if let scaledImage = scaledImage, let data = UIImageJPEGRepresentation(scaledImage, 0.6) { + subscriber.putNext(.dataPart(data: data, range: 0 ..< data.count, complete: true)) + subscriber.putCompletion() + } else { + subscriber.putCompletion() + } + } + } else { + if !madeProgress.swap(true) { + subscriber.putNext(.reset) + } + } + } + }) + } else { + subscriber.putNext(.reset) + } + + return ActionDisposable { + if let requestId = requestId { + PHImageManager.default().cancelImageRequest(requestId) + } + } + } +} diff --git a/TelegramUI/FetchVideoMediaResource.swift b/TelegramUI/FetchVideoMediaResource.swift index 36749766cd..b19a02a1e7 100644 --- a/TelegramUI/FetchVideoMediaResource.swift +++ b/TelegramUI/FetchVideoMediaResource.swift @@ -33,7 +33,6 @@ private final class VideoConversionWatcher: TGMediaVideoFileWatcher { func fetchVideoLibraryMediaResource(resource: VideoLibraryMediaResource) -> Signal { return Signal { subscriber in subscriber.putNext(.reset) - print("request video") let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [resource.localIdentifier], options: nil) var requestId: PHImageRequestID? @@ -116,10 +115,37 @@ func fetchLocalFileVideoMediaResource(resource: LocalFileVideoMediaResource) -> adjustments = TGVideoEditAdjustments(dictionary: dict) } } - let signal = TGMediaVideoConverter.convert(avAsset, adjustments: adjustments, watcher: nil)! + let updatedSize = Atomic(value: 0) + let signal = TGMediaVideoConverter.convert(avAsset, adjustments: adjustments, watcher: VideoConversionWatcher(update: { path, size in + var value = stat() + if stat(path, &value) == 0 { + if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) { + var range: Range? + let _ = updatedSize.modify { updatedSize in + range = updatedSize ..< Int(value.st_size) + return Int(value.st_size) + } + //print("size = \(Int(value.st_size)), range: \(range!)") + subscriber.putNext(.dataPart(data: data, range: range!, complete: false)) + } + } + }))! let signalDisposable = signal.start(next: { next in if let result = next as? TGMediaVideoConversionResult { - subscriber.putNext(.moveLocalFile(path: result.fileURL.path)) + var value = stat() + if stat(result.fileURL.path, &value) == 0 { + if let data = try? Data(contentsOf: result.fileURL, options: [.mappedRead]) { + var range: Range? + let _ = updatedSize.modify { updatedSize in + range = updatedSize ..< Int(value.st_size) + return Int(value.st_size) + } + //print("finish size = \(Int(value.st_size)), range: \(range!)") + subscriber.putNext(.dataPart(data: data, range: range!, complete: false)) + subscriber.putNext(.replaceHeader(data: data, range: 0 ..< 1024)) + subscriber.putNext(.dataPart(data: Data(), range: 0 ..< 0, complete: true)) + } + } subscriber.putCompletion() } }, error: { _ in diff --git a/TelegramUI/ForwardAccessoryPanelNode.swift b/TelegramUI/ForwardAccessoryPanelNode.swift index b8086b4135..6c2f4f2ea5 100644 --- a/TelegramUI/ForwardAccessoryPanelNode.swift +++ b/TelegramUI/ForwardAccessoryPanelNode.swift @@ -19,6 +19,63 @@ private let closeButtonImage = generateImage(CGSize(width: 12.0, height: 12.0), context.strokePath() }) +func textStringForForwardedMessage(_ message: Message) -> (String, Bool) { + if !message.text.isEmpty { + return (message.text, false) + } else { + for media in message.media { + switch media { + case _ as TelegramMediaImage: + return ("Forwarded photo", true) + case let file as TelegramMediaFile: + var fileName: String = "Forwarded file" + for attribute in file.attributes { + switch attribute { + case .Sticker: + return ("Forwarded sticker", true) + case let .FileName(name): + fileName = name + case let .Audio(isVoice, _, title, performer, _): + if isVoice { + return ("Forwarded voice Message", true) + } else { + if let title = title, let performer = performer, !title.isEmpty, !performer.isEmpty { + return (title + " — " + performer, true) + } else if let title = title, !title.isEmpty { + return (title, true) + } else if let performer = performer, !performer.isEmpty { + return (performer, true) + } else { + return ("Forwarded audio", true) + } + } + case .Video: + if file.isAnimated { + return ("Forwarded gIF", true) + } else { + return ("Forwarded video", true) + } + default: + break + } + } + return (fileName, true) + case _ as TelegramMediaContact: + return ("Forwarded contact", true) + case let game as TelegramMediaGame: + return (game.title, true) + case _ as TelegramMediaMap: + return ("Forwarded map", true) + case let action as TelegramMediaAction: + return ("", true) + default: + break + } + } + return ("", false) + } +} + final class ForwardAccessoryPanelNode: AccessoryPanelNode { private let messageDisposable = MetaDisposable() let messageIds: [MessageId] @@ -76,7 +133,8 @@ final class ForwardAccessoryPanelNode: AccessoryPanelNode { } } if messages.count == 1 { - text = messages[0].text + let (string, _) = textStringForForwardedMessage(messages[0]) + text = string } else { text = "\(messages.count) messages" } diff --git a/TelegramUI/GalleryController.swift b/TelegramUI/GalleryController.swift index 73598c55d6..e018a36844 100644 --- a/TelegramUI/GalleryController.swift +++ b/TelegramUI/GalleryController.swift @@ -71,7 +71,7 @@ private func mediaForMessage(message: Message) -> Media? { func galleryItemForEntry(account: Account, entry: MessageHistoryEntry) -> GalleryItem { switch entry { - case let .MessageEntry(message, _, location): + case let .MessageEntry(message, _, location, _): if let media = mediaForMessage(message: message) { if let _ = media as? TelegramMediaImage { return ChatImageGalleryItem(account: account, message: message, location: location) @@ -165,7 +165,7 @@ class GalleryController: ViewController { self.navigationBar.foregroundColor = UIColor.white self.navigationBar.accentColor = UIColor.white - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Done", style: .plain, target: self, action: #selector(self.donePressed)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(self.donePressed)) self.statusBar.statusBarStyle = .White @@ -175,7 +175,7 @@ class GalleryController: ViewController { |> filter({ $0 != nil }) |> mapToSignal { message -> Signal in if let tags = tagsForMessage(message!) { - let view = account.postbox.aroundMessageHistoryViewForPeerId(messageId.peerId, index: MessageIndex(message!), count: 50, anchorIndex: MessageIndex(message!), fixedCombinedReadState: nil, topTaggedMessageIdNamespaces: [], tagMask: tags) + let view = account.postbox.aroundMessageHistoryViewForPeerId(messageId.peerId, index: MessageIndex(message!), count: 50, anchorIndex: MessageIndex(message!), fixedCombinedReadState: nil, topTaggedMessageIdNamespaces: [], tagMask: tags, orderStatistics: [.combinedLocation]) return view |> mapToSignal { (view, _, _) -> Signal in @@ -183,7 +183,7 @@ class GalleryController: ViewController { return .single(mapped) } } else { - return .single(GalleryMessageHistoryView.single(MessageHistoryEntry.MessageEntry(message!, false, nil))) + return .single(GalleryMessageHistoryView.single(MessageHistoryEntry.MessageEntry(message!, false, nil, nil))) } } |> take(1) @@ -195,7 +195,7 @@ class GalleryController: ViewController { strongSelf.entries = view.entries loop: for i in 0 ..< strongSelf.entries.count { switch strongSelf.entries[i] { - case let .MessageEntry(message, _, _) where message.id == messageId: + case let .MessageEntry(message, _, _, _) where message.id == messageId: strongSelf.centralEntryIndex = i break loop default: @@ -267,7 +267,7 @@ class GalleryController: ViewController { } if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments { - if case let .MessageEntry(message, _, _) = self.entries[centralItemNode.index] { + if case let .MessageEntry(message, _, _, _) = self.entries[centralItemNode.index] { if let media = mediaForMessage(message: message), let transitionArguments = presentationArguments.transitionArguments(message.id, media) { animatedOutNode = false centralItemNode.animateOut(to: transitionArguments.transitionNode, completion: { @@ -294,7 +294,7 @@ class GalleryController: ViewController { self.galleryNode.transitionNodeForCentralItem = { [weak self] in if let strongSelf = self { if let centralItemNode = strongSelf.galleryNode.pager.centralItemNode(), let presentationArguments = strongSelf.presentationArguments as? GalleryControllerPresentationArguments { - if case let .MessageEntry(message, _, _) = strongSelf.entries[centralItemNode.index] { + if case let .MessageEntry(message, _, _, _) = strongSelf.entries[centralItemNode.index] { if let media = mediaForMessage(message: message), let transitionArguments = presentationArguments.transitionArguments(message.id, media) { return transitionArguments.transitionNode } @@ -314,7 +314,7 @@ class GalleryController: ViewController { if let strongSelf = self { var hiddenItem: (MessageId, Media)? if let index = index { - if case let .MessageEntry(message, _, _) = strongSelf.entries[index], let media = mediaForMessage(message: message) { + if case let .MessageEntry(message, _, _, _) = strongSelf.entries[index], let media = mediaForMessage(message: message) { hiddenItem = (message.id, media) } @@ -337,7 +337,7 @@ class GalleryController: ViewController { var nodeAnimatesItself = false if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? GalleryControllerPresentationArguments { - if case let .MessageEntry(message, _, _) = self.entries[centralItemNode.index] { + if case let .MessageEntry(message, _, _, _) = self.entries[centralItemNode.index] { self.centralItemTitle.set(centralItemNode.title()) self.centralItemTitleView.set(centralItemNode.titleView()) self.centralItemNavigationStyle.set(centralItemNode.navigationStyle()) diff --git a/TelegramUI/GridMessageItem.swift b/TelegramUI/GridMessageItem.swift index e69207c2ab..66892fec33 100644 --- a/TelegramUI/GridMessageItem.swift +++ b/TelegramUI/GridMessageItem.swift @@ -3,6 +3,7 @@ import Display import AsyncDisplayKit import TelegramCore import Postbox +import SwiftSignalKit private func mediaForMessage(_ message: Message) -> Media? { for media in message.media { @@ -21,17 +22,90 @@ private func mediaForMessage(_ message: Message) -> Media? { return nil } +private let timezoneOffset: Int32 = { + let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + var now: time_t = time_t(nowTimestamp) + var timeinfoNow: tm = tm() + localtime_r(&now, &timeinfoNow) + return Int32(timeinfoNow.tm_gmtoff) +}() + +final class GridMessageItemSection: GridSection { + let height: CGFloat = 44.0 + + private let roundedTimestamp: Int32 + private let month: Int32 + private let year: Int32 + + var hashValue: Int { + return self.roundedTimestamp.hashValue + } + + init(timestamp: Int32) { + var now = time_t(timestamp) + var timeinfoNow: tm = tm() + localtime_r(&now, &timeinfoNow) + + self.roundedTimestamp = timeinfoNow.tm_year * 100 + timeinfoNow.tm_mon + self.month = timeinfoNow.tm_mon + self.year = timeinfoNow.tm_year + } + + func isEqual(to: GridSection) -> Bool { + if let to = to as? GridMessageItemSection { + return self.roundedTimestamp == to.roundedTimestamp + } else { + return false + } + } + + func node() -> ASDisplayNode { + return GridMessageItemSectionNode(roundedTimestamp: self.roundedTimestamp, month: self.month, year: self.year) + } +} + +private let sectionTitleFont = Font.regular(17.0) + +final class GridMessageItemSectionNode: ASDisplayNode { + let titleNode: ASTextNode + + init(roundedTimestamp: Int32, month: Int32, year: Int32) { + self.titleNode = ASTextNode() + self.titleNode.isLayerBacked = true + + super.init() + + self.backgroundColor = UIColor(white: 1.0, alpha: 0.9) + + let dateText = stringForMonth(month, ofYear: year) + self.addSubnode(self.titleNode) + self.titleNode.attributedText = NSAttributedString(string: dateText, font: sectionTitleFont, textColor: .black) + self.titleNode.maximumNumberOfLines = 1 + self.titleNode.truncationMode = .byTruncatingTail + } + + override func layout() { + super.layout() + + let bounds = self.bounds + + let titleSize = self.titleNode.measure(CGSize(width: bounds.size.width - 24.0, height: CGFloat.greatestFiniteMagnitude)) + self.titleNode.frame = CGRect(origin: CGPoint(x: 12.0, y: 18.0), size: titleSize) + } +} + final class GridMessageItem: GridItem { private let account: Account private let message: Message private let controllerInteraction: ChatControllerInteraction - let section: GridSection? = nil + let section: GridSection? init(account: Account, message: Message, controllerInteraction: ChatControllerInteraction) { self.account = account self.message = message self.controllerInteraction = controllerInteraction + self.section = GridMessageItemSection(timestamp: message.timestamp) } func node(layout: GridNodeLayout) -> GridItemNode { @@ -58,17 +132,29 @@ final class GridMessageItemNode: GridItemNode { private let imageNode: TransformImageNode private var messageId: MessageId? private var controllerInteraction: ChatControllerInteraction? + private var progressNode: RadialProgressNode private var selectionNode: GridMessageSelectionNode? + private let fetchStatusDisposable = MetaDisposable() + private let fetchDisposable = MetaDisposable() + private var resourceStatus: MediaResourceStatus? + override init() { self.imageNode = TransformImageNode() + self.progressNode = RadialProgressNode(theme: RadialProgressTheme(backgroundColor: UIColor(white: 0.0, alpha: 0.6), foregroundColor: UIColor.white, icon: nil)) + self.progressNode.isUserInteractionEnabled = false super.init() self.addSubnode(self.imageNode) } + deinit { + self.fetchStatusDisposable.dispose() + self.fetchDisposable.dispose() + } + override func didLoad() { super.didLoad() @@ -81,6 +167,35 @@ final class GridMessageItemNode: GridItemNode { if let image = media as? TelegramMediaImage, let largestSize = largestImageRepresentation(image.representations)?.dimensions { mediaDimensions = largestSize self.imageNode.setSignal(account: account, signal: mediaGridMessagePhoto(account: account, photo: image), dispatchOnDisplayLink: true) + + self.fetchStatusDisposable.set(nil) + self.progressNode.removeFromSupernode() + self.progressNode.isHidden = true + self.resourceStatus = nil + } else if let file = media as? TelegramMediaFile, file.isVideo { + mediaDimensions = file.dimensions + self.imageNode.setSignal(account: account, signal: mediaGridMessageVideo(account: account, video: file)) + + self.resourceStatus = nil + self.fetchStatusDisposable.set((account.postbox.mediaBox.resourceStatus(file.resource) |> deliverOnMainQueue).start(next: { [weak self] status in + if let strongSelf = self { + strongSelf.resourceStatus = status + switch status { + case let .Fetching(progress): + strongSelf.progressNode.state = .Fetching(progress: progress) + strongSelf.progressNode.isHidden = false + case .Local: + strongSelf.progressNode.state = .None + strongSelf.progressNode.isHidden = true + case .Remote: + strongSelf.progressNode.state = .Remote + strongSelf.progressNode.isHidden = false + } + } + })) + if self.progressNode.supernode == nil { + self.addSubnode(self.progressNode) + } } if let mediaDimensions = mediaDimensions { @@ -108,12 +223,14 @@ final class GridMessageItemNode: GridItemNode { } self.selectionNode?.frame = CGRect(origin: CGPoint(), size: self.bounds.size) + let progressDiameter: CGFloat = 40.0 + self.progressNode.frame = CGRect(origin: CGPoint(x: imageFrame.minX + floor((imageFrame.size.width - progressDiameter) / 2.0), y: imageFrame.minY + floor((imageFrame.size.height - progressDiameter) / 2.0)), size: CGSize(width: progressDiameter, height: progressDiameter)) } func updateSelectionState(animated: Bool) { if let messageId = self.messageId, let controllerInteraction = self.controllerInteraction { if let selectionState = controllerInteraction.selectionState { - var selected = selectionState.selectedIds.contains(messageId) + let selected = selectionState.selectedIds.contains(messageId) if let selectionNode = self.selectionNode { selectionNode.updateSelected(selected, animated: animated) @@ -166,7 +283,22 @@ final class GridMessageItemNode: GridItemNode { @objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) { if let controllerInteraction = self.controllerInteraction, let messageId = self.messageId, case .ended = recognizer.state { - controllerInteraction.openMessage(messageId) + if let (account, media, _) = self.currentState { + if let file = media as? TelegramMediaFile { + if let resourceStatus = self.resourceStatus { + switch resourceStatus { + case .Fetching: + account.postbox.mediaBox.cancelInteractiveResourceFetch(file.resource) + case .Local: + controllerInteraction.openMessage(messageId) + case .Remote: + self.fetchDisposable.set(account.postbox.mediaBox.fetchedResource(file.resource).start()) + } + } + } else { + controllerInteraction.openMessage(messageId) + } + } } } } diff --git a/TelegramUI/HashtagChatInputPanelItem.swift b/TelegramUI/HashtagChatInputPanelItem.swift index fa75561a26..b2abfdce02 100644 --- a/TelegramUI/HashtagChatInputPanelItem.swift +++ b/TelegramUI/HashtagChatInputPanelItem.swift @@ -115,7 +115,7 @@ final class HashtagChatInputPanelItemNode: ListViewItemNode { let leftInset: CGFloat = 15.0 let rightInset: CGFloat = 10.0 - let (textLayout, textApply) = makeTextLayout(NSAttributedString(string: item.text, font: textFont, textColor: .black), nil, 1, .end, CGSize(width: width - leftInset - rightInset, height: 100.0), .natural, nil) + let (textLayout, textApply) = makeTextLayout(NSAttributedString(string: item.text, font: textFont, textColor: .black), nil, 1, .end, CGSize(width: width - leftInset - rightInset, height: 100.0), .natural, nil, UIEdgeInsets()) let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: HashtagChatInputPanelItemNode.itemHeight), insets: UIEdgeInsets()) diff --git a/TelegramUI/HorizontalPeerItem.swift b/TelegramUI/HorizontalPeerItem.swift index 994a2954fa..23ba1ea694 100644 --- a/TelegramUI/HorizontalPeerItem.swift +++ b/TelegramUI/HorizontalPeerItem.swift @@ -35,10 +35,10 @@ final class HorizontalPeerItem: ListViewItem { } } -private final class HorizontalPeerItemNode: ListViewItemNode { +final class HorizontalPeerItemNode: ListViewItemNode { private let avatarNode: AvatarNode private let titleNode: ASTextNode - private var peer: Peer? + private(set) var peer: Peer? fileprivate var action: ((Peer) -> Void)? init() { diff --git a/TelegramUI/ItemListActionItem.swift b/TelegramUI/ItemListActionItem.swift index e7adfefb98..1028c7f912 100644 --- a/TelegramUI/ItemListActionItem.swift +++ b/TelegramUI/ItemListActionItem.swift @@ -132,7 +132,7 @@ class ItemListActionItemNode: ListViewItemNode { textColor = UIColor(0x8e8e93) } - let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: textColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: textColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let contentSize: CGSize let insets: UIEdgeInsets diff --git a/TelegramUI/ItemListActivityTextItem.swift b/TelegramUI/ItemListActivityTextItem.swift index d19df24094..05b1d3023e 100644 --- a/TelegramUI/ItemListActivityTextItem.swift +++ b/TelegramUI/ItemListActivityTextItem.swift @@ -104,7 +104,7 @@ class ItemListActivityTextItemNode: ListViewItemNode { titleString.removeAttribute(NSFontAttributeName, range: NSMakeRange(0, titleString.length)) titleString.addAttributes([NSFontAttributeName: titleFont], range: NSMakeRange(0, titleString.length)) - let (titleLayout, titleApply) = makeTitleLayout(titleString, nil, 0, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, TextNodeCutout(position: .TopLeft, size: CGSize(width: activityWidth, height: 4.0))) + let (titleLayout, titleApply) = makeTitleLayout(titleString, nil, 0, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, TextNodeCutout(position: .TopLeft, size: CGSize(width: activityWidth, height: 4.0)), UIEdgeInsets()) let contentSize: CGSize let insets: UIEdgeInsets diff --git a/TelegramUI/ItemListAvatarAndNameItem.swift b/TelegramUI/ItemListAvatarAndNameItem.swift index 01e9738d2e..c8e30402d0 100644 --- a/TelegramUI/ItemListAvatarAndNameItem.swift +++ b/TelegramUI/ItemListAvatarAndNameItem.swift @@ -205,7 +205,7 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { displayTitle = .title(title: "") } - let (nameNodeLayout, nameNodeApply) = layoutNameNode(NSAttributedString(string: displayTitle.composedTitle, font: nameFont, textColor: UIColor.black), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (nameNodeLayout, nameNodeApply) = layoutNameNode(NSAttributedString(string: displayTitle.composedTitle, font: nameFont, textColor: UIColor.black), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let statusText: String let statusColor: UIColor @@ -240,7 +240,7 @@ class ItemListAvatarAndNameInfoItemNode: ListViewItemNode { statusColor = UIColor.black } - let (statusNodeLayout, statusNodeApply) = layoutStatusNode(NSAttributedString(string: statusText, font: statusFont, textColor: statusColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (statusNodeLayout, statusNodeApply) = layoutStatusNode(NSAttributedString(string: statusText, font: statusFont, textColor: statusColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let separatorHeight = UIScreenPixel diff --git a/TelegramUI/ItemListCheckboxItem.swift b/TelegramUI/ItemListCheckboxItem.swift index 2a9cf794e8..04e922c82f 100644 --- a/TelegramUI/ItemListCheckboxItem.swift +++ b/TelegramUI/ItemListCheckboxItem.swift @@ -117,7 +117,7 @@ class ItemListCheckboxItemNode: ListViewItemNode { return { item, width, neighbors in let leftInset: CGFloat = 44.0 - let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: UIColor.black), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: UIColor.black), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let separatorHeight = UIScreenPixel diff --git a/TelegramUI/ItemListDisclosureItem.swift b/TelegramUI/ItemListDisclosureItem.swift index 68ce1d8130..e4417d41bc 100644 --- a/TelegramUI/ItemListDisclosureItem.swift +++ b/TelegramUI/ItemListDisclosureItem.swift @@ -126,8 +126,8 @@ class ItemListDisclosureItemNode: ListViewItemNode { insets = itemListNeighborsGroupedInsets(neighbors) } - let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: UIColor.black), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil) - let (labelLayout, labelApply) = makeLabelLayout(NSAttributedString(string: item.label, font: titleFont, textColor: UIColor(0x8e8e93)), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: UIColor.black), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (labelLayout, labelApply) = makeLabelLayout(NSAttributedString(string: item.label, font: titleFont, textColor: UIColor(0x8e8e93)), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layoutSize = layout.size diff --git a/TelegramUI/ItemListMultilineInputItem.swift b/TelegramUI/ItemListMultilineInputItem.swift index 50d9ac7280..e5eda41435 100644 --- a/TelegramUI/ItemListMultilineInputItem.swift +++ b/TelegramUI/ItemListMultilineInputItem.swift @@ -117,7 +117,7 @@ class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNodeDelega } let attributedMeasureText = NSAttributedString(string: measureText, font: Font.regular(17.0), textColor: .black) let attributedText = NSAttributedString(string: item.text, font: Font.regular(17.0), textColor: .black) - let (textLayout, textApply) = makeTextLayout(attributedMeasureText, nil, 0, .end, CGSize(width: width - 8 - leftInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (textLayout, textApply) = makeTextLayout(attributedMeasureText, nil, 0, .end, CGSize(width: width - 8 - leftInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let separatorHeight = UIScreenPixel diff --git a/TelegramUI/ItemListMultilineTextItem.swift b/TelegramUI/ItemListMultilineTextItem.swift index 19e0486473..d208890b60 100644 --- a/TelegramUI/ItemListMultilineTextItem.swift +++ b/TelegramUI/ItemListMultilineTextItem.swift @@ -98,7 +98,7 @@ class ItemListMultilineTextItemNode: ListViewItemNode { leftInset = 16.0 } - let (titleLayout, titleApply) = makeTextLayout(NSAttributedString(string: item.text, font: titleFont, textColor: textColor), nil, 0, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (titleLayout, titleApply) = makeTextLayout(NSAttributedString(string: item.text, font: titleFont, textColor: textColor), nil, 0, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let contentSize: CGSize let insets: UIEdgeInsets diff --git a/TelegramUI/ItemListPeerActionItem.swift b/TelegramUI/ItemListPeerActionItem.swift index 3f5ad1bcc8..1d7f2a75e3 100644 --- a/TelegramUI/ItemListPeerActionItem.swift +++ b/TelegramUI/ItemListPeerActionItem.swift @@ -114,7 +114,7 @@ class ItemListPeerActionItemNode: ListViewItemNode { let editingOffset: CGFloat = (item.editing ? 38.0 : 0.0) - let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: UIColor(0x007ee5)), nil, 1, .end, CGSize(width: width - leftInset - editingOffset, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: UIColor(0x007ee5)), nil, 1, .end, CGSize(width: width - leftInset - editingOffset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let separatorHeight = UIScreenPixel diff --git a/TelegramUI/ItemListPeerItem.swift b/TelegramUI/ItemListPeerItem.swift index 05843fafb2..cd8e604bf2 100644 --- a/TelegramUI/ItemListPeerItem.swift +++ b/TelegramUI/ItemListPeerItem.swift @@ -276,10 +276,10 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { editingOffset = 0.0 } - let (labelLayout, labelApply) = makeLabelLayout(labelAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (labelLayout, labelApply) = makeLabelLayout(labelAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) - let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - labelLayout.size.width - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil) - let (statusLayout, statusApply) = makeStatusLayout(statusAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - (labelLayout.size.width > 0.0 ? (labelLayout.size.width) + 15.0 : 0.0) - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - labelLayout.size.width - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (statusLayout, statusApply) = makeStatusLayout(statusAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - (labelLayout.size.width > 0.0 ? (labelLayout.size.width) + 15.0 : 0.0) - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let insets = itemListNeighborsGroupedInsets(neighbors) let contentSize = CGSize(width: width, height: 48.0) diff --git a/TelegramUI/ItemListRecentSessionItem.swift b/TelegramUI/ItemListRecentSessionItem.swift index 3bb70ac698..9e89597430 100644 --- a/TelegramUI/ItemListRecentSessionItem.swift +++ b/TelegramUI/ItemListRecentSessionItem.swift @@ -220,10 +220,10 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode { editingOffset = 0.0 } - let (labelLayout, labelApply) = makeLabelLayout(labelAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil) - let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - editingOffset - rightInset - labelLayout.size.width - 5.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil) - let (appLayout, appApply) = makeAppLayout(appAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil) - let (locationLayout, locationApply) = makeLocationLayout(locationAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (labelLayout, labelApply) = makeLabelLayout(labelAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - editingOffset - rightInset - labelLayout.size.width - 5.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (appLayout, appApply) = makeAppLayout(appAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (locationLayout, locationApply) = makeLocationLayout(locationAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let insets = itemListNeighborsGroupedInsets(neighbors) let contentSize = CGSize(width: width, height: 75.0) diff --git a/TelegramUI/ItemListSectionHeaderItem.swift b/TelegramUI/ItemListSectionHeaderItem.swift index 78c9e6a7b9..b439c02e74 100644 --- a/TelegramUI/ItemListSectionHeaderItem.swift +++ b/TelegramUI/ItemListSectionHeaderItem.swift @@ -71,7 +71,7 @@ class ItemListSectionHeaderItemNode: ListViewItemNode { return { item, width, neighbors in let leftInset: CGFloat = 15.0 - let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.text, font: titleFont, textColor: UIColor(0x6d6d72)), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.text, font: titleFont, textColor: UIColor(0x6d6d72)), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let contentSize: CGSize var insets = UIEdgeInsets() diff --git a/TelegramUI/ItemListSingleLineInputItem.swift b/TelegramUI/ItemListSingleLineInputItem.swift index afdfba316b..011ee2cdb9 100644 --- a/TelegramUI/ItemListSingleLineInputItem.swift +++ b/TelegramUI/ItemListSingleLineInputItem.swift @@ -125,7 +125,7 @@ class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDelegate, It titleString.removeAttribute(NSFontAttributeName, range: NSMakeRange(0, titleString.length)) titleString.addAttributes([NSFontAttributeName: Font.regular(17.0)], range: NSMakeRange(0, titleString.length)) - let (titleLayout, titleApply) = makeTitleLayout(titleString, nil, 0, .end, CGSize(width: width - 32 - leftInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (titleLayout, titleApply) = makeTitleLayout(titleString, nil, 0, .end, CGSize(width: width - 32 - leftInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let separatorHeight = UIScreenPixel diff --git a/TelegramUI/ItemListStickerPackItem.swift b/TelegramUI/ItemListStickerPackItem.swift index d992cd0b91..508f5496eb 100644 --- a/TelegramUI/ItemListStickerPackItem.swift +++ b/TelegramUI/ItemListStickerPackItem.swift @@ -296,8 +296,8 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { editingOffset = 0.0 } - let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - editingOffset - rightInset - 10.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil) - let (statusLayout, statusApply) = makeStatusLayout(statusAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - editingOffset - rightInset - 10.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (statusLayout, statusApply) = makeStatusLayout(statusAttributedString, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - editingOffset - rightInset, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let insets = itemListNeighborsGroupedInsets(neighbors) let contentSize = CGSize(width: width, height: 59.0) diff --git a/TelegramUI/ItemListSwitchItem.swift b/TelegramUI/ItemListSwitchItem.swift index 4ef42cf5b8..774ccb23f9 100644 --- a/TelegramUI/ItemListSwitchItem.swift +++ b/TelegramUI/ItemListSwitchItem.swift @@ -115,7 +115,7 @@ class ItemListSwitchItemNode: ListViewItemNode { insets = itemListNeighborsGroupedInsets(neighbors) } - let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: UIColor.black), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (titleLayout, titleApply) = makeTitleLayout(NSAttributedString(string: item.title, font: titleFont, textColor: UIColor.black), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layoutSize = layout.size diff --git a/TelegramUI/ItemListTextItem.swift b/TelegramUI/ItemListTextItem.swift index 3bf2ddc218..6f5e14b34d 100644 --- a/TelegramUI/ItemListTextItem.swift +++ b/TelegramUI/ItemListTextItem.swift @@ -104,7 +104,7 @@ class ItemListTextItemNode: ListViewItemNode { return (TextNode.UrlAttribute, contents) })) } - let (titleLayout, titleApply) = makeTitleLayout(attributedText, nil, 0, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (titleLayout, titleApply) = makeTitleLayout(attributedText, nil, 0, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let contentSize: CGSize diff --git a/TelegramUI/ItemListTextWithLabelItem.swift b/TelegramUI/ItemListTextWithLabelItem.swift index cf6fa51b68..d9eaa91f7e 100644 --- a/TelegramUI/ItemListTextWithLabelItem.swift +++ b/TelegramUI/ItemListTextWithLabelItem.swift @@ -8,12 +8,16 @@ final class ItemListTextWithLabelItem: ListViewItem, ItemListItem { let text: String let multiline: Bool let sectionId: ItemListSectionId + let action: (() -> Void)? + let tag: Any? - init(label: String, text: String, multiline: Bool, sectionId: ItemListSectionId) { + init(label: String, text: String, multiline: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, tag: Any? = nil) { self.label = label self.text = text self.multiline = multiline self.sectionId = sectionId + self.action = action + self.tag = tag } func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { @@ -47,10 +51,13 @@ final class ItemListTextWithLabelItem: ListViewItem, ItemListItem { } } - var selectable: Bool = true + var selectable: Bool { + return self.action != nil + } func selected(listView: ListView){ - + listView.clearHighlightAnimated(true) + self.action?() } } @@ -60,9 +67,31 @@ private let textFont = Font.regular(17.0) class ItemListTextWithLabelItemNode: ListViewItemNode { let labelNode: TextNode let textNode: TextNode - let separatorNode: ASDisplayNode + + private let backgroundNode: ASDisplayNode + private let topStripeNode: ASDisplayNode + private let bottomStripeNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + + var item: ItemListTextWithLabelItem? init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.backgroundColor = .white + + self.topStripeNode = ASDisplayNode() + self.topStripeNode.backgroundColor = UIColor(0xc8c7cc) + self.topStripeNode.isLayerBacked = true + + self.bottomStripeNode = ASDisplayNode() + self.bottomStripeNode.backgroundColor = UIColor(0xc8c7cc) + self.bottomStripeNode.isLayerBacked = true + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) + self.highlightedBackgroundNode.isLayerBacked = true + self.labelNode = TextNode() self.labelNode.isLayerBacked = true self.labelNode.contentMode = .left @@ -73,14 +102,8 @@ class ItemListTextWithLabelItemNode: ListViewItemNode { self.textNode.contentMode = .left self.textNode.contentsScale = UIScreen.main.scale - self.separatorNode = ASDisplayNode() - self.separatorNode.isLayerBacked = true - self.separatorNode.displaysAsynchronously = false - self.separatorNode.backgroundColor = UIColor(0xc8c7cc) - super.init(layerBacked: false, dynamicBounce: false) - self.addSubnode(self.separatorNode) self.addSubnode(self.labelNode) self.addSubnode(self.textNode) } @@ -92,24 +115,117 @@ class ItemListTextWithLabelItemNode: ListViewItemNode { return { item, width, neighbors in let insets = itemListNeighborsPlainInsets(neighbors) let leftInset: CGFloat = 35.0 + let separatorHeight = UIScreenPixel - let (labelLayout, labelApply) = makeLabelLayout(NSAttributedString(string: item.label, font: labelFont, textColor: UIColor(0x007ee5)), nil, 1, .end, CGSize(width: width - leftInset - 8.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil) - let (textLayout, textApply) = makeTextLayout(NSAttributedString(string: item.text, font: textFont, textColor: UIColor.black), nil, item.multiline ? 0 : 1, .end, CGSize(width: width - leftInset - 8.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (labelLayout, labelApply) = makeLabelLayout(NSAttributedString(string: item.label, font: labelFont, textColor: UIColor(0x007ee5)), nil, 1, .end, CGSize(width: width - leftInset - 8.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) + let (textLayout, textApply) = makeTextLayout(NSAttributedString(string: item.text, font: textFont, textColor: UIColor.black), nil, item.multiline ? 0 : 1, .end, CGSize(width: width - leftInset - 8.0, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let contentSize = CGSize(width: width, height: textLayout.size.height + 39.0) - return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in + let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) + let layoutSize = nodeLayout.size + return (nodeLayout, { [weak self] in if let strongSelf = self { + strongSelf.item = item + let _ = labelApply() let _ = textApply() strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: labelLayout.size) strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 31.0), size: textLayout.size) - strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - UIScreenPixel), size: CGSize(width: width - leftInset, height: UIScreenPixel)) + let leftInset: CGFloat + let style = ItemListStyle.plain + switch style { + case .plain: + leftInset = 35.0 + + if strongSelf.backgroundNode.supernode != nil { + strongSelf.backgroundNode.removeFromSupernode() + } + if strongSelf.topStripeNode.supernode != nil { + strongSelf.topStripeNode.removeFromSupernode() + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0) + } + + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: width - leftInset, height: separatorHeight)) + case .blocks: + leftInset = 16.0 + + if strongSelf.backgroundNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) + } + if strongSelf.topStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) + } + if strongSelf.bottomStripeNode.supernode == nil { + strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) + } + switch neighbors.top { + case .sameSection(false): + strongSelf.topStripeNode.isHidden = true + default: + strongSelf.topStripeNode.isHidden = false + } + let bottomStripeInset: CGFloat + let bottomStripeOffset: CGFloat + switch neighbors.bottom { + case .sameSection(false): + bottomStripeInset = 16.0 + bottomStripeOffset = -separatorHeight + default: + bottomStripeInset = 0.0 + bottomStripeOffset = 0.0 + } + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) + strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) + strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) + } + + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: width, height: contentSize.height + UIScreenPixel + UIScreenPixel)) } }) } } + override func setHighlighted(_ highlighted: Bool, animated: Bool) { + super.setHighlighted(highlighted, animated: animated) + + if highlighted { + self.highlightedBackgroundNode.alpha = 1.0 + if self.highlightedBackgroundNode.supernode == nil { + var anchorNode: ASDisplayNode? + if self.bottomStripeNode.supernode != nil { + anchorNode = self.bottomStripeNode + } else if self.topStripeNode.supernode != nil { + anchorNode = self.topStripeNode + } else if self.backgroundNode.supernode != nil { + anchorNode = self.backgroundNode + } + if let anchorNode = anchorNode { + self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode) + } else { + self.addSubnode(self.highlightedBackgroundNode) + } + } + } else { + if self.highlightedBackgroundNode.supernode != nil { + if animated { + self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in + if let strongSelf = self { + if completed { + strongSelf.highlightedBackgroundNode.removeFromSupernode() + } + } + }) + self.highlightedBackgroundNode.alpha = 0.0 + } else { + self.highlightedBackgroundNode.removeFromSupernode() + } + } + } + } + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } @@ -117,4 +233,8 @@ class ItemListTextWithLabelItemNode: ListViewItemNode { override func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } + + var tag: Any? { + return self.item?.tag + } } diff --git a/TelegramUI/LegacyAttachmentMenu.swift b/TelegramUI/LegacyAttachmentMenu.swift index 7999c46468..e4bde209df 100644 --- a/TelegramUI/LegacyAttachmentMenu.swift +++ b/TelegramUI/LegacyAttachmentMenu.swift @@ -31,7 +31,7 @@ func legacyAttachmentMenu(parentController: LegacyController, recentlyUsedInline sendMessagesWithSignals(signals) } }; - carouselItem.allowCaptions = false + carouselItem.allowCaptions = true itemViews.append(carouselItem) let galleryItem = TGMenuSheetButtonItemView(title: "Photo or Video", type: TGMenuSheetButtonTypeDefault, action: { [weak controller] in diff --git a/TelegramUI/LegacyMediaPickers.swift b/TelegramUI/LegacyMediaPickers.swift index cba222ac4f..d7f62af041 100644 --- a/TelegramUI/LegacyMediaPickers.swift +++ b/TelegramUI/LegacyMediaPickers.swift @@ -8,7 +8,7 @@ import UIKit import Display func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, captionsEnabled: Bool = true, storeCreatedAssets: Bool = true, showFileTooltip: Bool = false) { - controller.captionsEnabled = false//captionsEnabled + controller.captionsEnabled = captionsEnabled controller.inhibitDocumentCaptions = false controller.suggestionContext = nil controller.dismissalBlock = { @@ -67,9 +67,9 @@ private enum LegacyAssetVideoData { } private enum LegacyAssetItem { - case image(LegacyAssetImageData) - case file(LegacyAssetImageData, mimeType: String, name: String) - case video(LegacyAssetVideoData, UIImage?, TGVideoEditAdjustments?) + case image(data: LegacyAssetImageData, caption: String?) + case file(data: LegacyAssetImageData, mimeType: String, name: String, caption: String?) + case video(data: LegacyAssetVideoData, previewImage: UIImage?, adjustments: TGVideoEditAdjustments?, caption: String?) } private final class LegacyAssetItemWrapper: NSObject { @@ -88,7 +88,7 @@ func legacyAssetPickerItemGenerator() -> ((Any?, String?, String?) -> [AnyHashab if (dict["type"] as! NSString) == "editedPhoto" || (dict["type"] as! NSString) == "capturedPhoto" { let image = dict["image"] as! UIImage var result: [AnyHashable : Any] = [:] - result["item" as NSString] = LegacyAssetItemWrapper(item: .image(.image(image))) + result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .image(image), caption: caption)) return result } else if (dict["type"] as! NSString) == "cloudPhoto" { let asset = dict["asset"] as! TGMediaAsset @@ -101,7 +101,7 @@ func legacyAssetPickerItemGenerator() -> ((Any?, String?, String?) -> [AnyHashab //result["item" as NSString] = LegacyAssetItemWrapper(item: .file(.asset(asset.backingAsset))) return nil } else { - result["item" as NSString] = LegacyAssetItemWrapper(item: .image(.asset(asset.backingAsset))) + result["item" as NSString] = LegacyAssetItemWrapper(item: .image(data: .asset(asset.backingAsset), caption: caption)) } return result } else if (dict["type"] as! NSString) == "file" { @@ -116,19 +116,19 @@ func legacyAssetPickerItemGenerator() -> ((Any?, String?, String?) -> [AnyHashab } var result: [AnyHashable: Any] = [:] - result["item" as NSString] = LegacyAssetItemWrapper(item: .file(.tempFile(tempFileUrl.path), mimeType: mimeType, name: name)) + result["item" as NSString] = LegacyAssetItemWrapper(item: .file(data: .tempFile(tempFileUrl.path), mimeType: mimeType, name: name, caption: caption)) return result } } else if (dict["type"] as! NSString) == "video" { if let asset = dict["asset"] as? TGMediaAsset { var result: [AnyHashable: Any] = [:] - result["item" as NSString] = LegacyAssetItemWrapper(item: .video(.asset(asset), dict["previewImage"] as? UIImage, dict["adjustments"] as? TGVideoEditAdjustments)) + result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .asset(asset), previewImage: dict["previewImage"] as? UIImage, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption)) return result } else if let url = dict["url"] as? String { let dimensions = (dict["dimensions"]! as AnyObject).cgSizeValue! let duration = (dict["duration"]! as AnyObject).doubleValue! var result: [AnyHashable: Any] = [:] - result["item" as NSString] = LegacyAssetItemWrapper(item: .video(.tempFile(path: url, dimensions: dimensions, duration: duration), dict["previewImage"] as? UIImage, dict["adjustments"] as? TGVideoEditAdjustments)) + result["item" as NSString] = LegacyAssetItemWrapper(item: .video(data: .tempFile(path: url, dimensions: dimensions, duration: duration), previewImage: dict["previewImage"] as? UIImage, adjustments: dict["adjustments"] as? TGVideoEditAdjustments, caption: caption)) return result } } @@ -144,7 +144,7 @@ func legacyAssetPickerEnqueueMessages(account: Account, peerId: PeerId, signals: for item in (anyValues as! NSArray) { if let item = (item as? NSDictionary)?.object(forKey: "item") as? LegacyAssetItemWrapper { switch item.item { - case let .image(data): + case let .image(data, caption): switch data { case let .image(image): var randomId: Int64 = 0 @@ -158,7 +158,7 @@ func legacyAssetPickerEnqueueMessages(account: Account, peerId: PeerId, signals: let _ = try? scaledImageData.write(to: URL(fileURLWithPath: tempFilePath)) let resource = LocalFileReferenceMediaResource(localFilePath: tempFilePath, randomId: randomId) let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: scaledSize, resource: resource)]) - messages.append(.message(text: "", attributes: [], media: media, replyToMessageId: nil)) + messages.append(.message(text: caption ?? "", attributes: [], media: media, replyToMessageId: nil)) } } case let .asset(asset): @@ -169,22 +169,22 @@ func legacyAssetPickerEnqueueMessages(account: Account, peerId: PeerId, signals: let resource = PhotoLibraryMediaResource(localIdentifier: asset.localIdentifier) let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: scaledSize, resource: resource)]) - messages.append(.message(text: "", attributes: [], media: media, replyToMessageId: nil)) + messages.append(.message(text: caption ?? "", attributes: [], media: media, replyToMessageId: nil)) case .tempFile: break } - case let .file(data, mimeType, name): + case let .file(data, mimeType, name, caption): switch data { case let .tempFile(path): var randomId: Int64 = 0 arc4random_buf(&randomId, 8) let resource = LocalFileReferenceMediaResource(localFilePath: path, randomId: randomId) let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), resource: resource, previewRepresentations: [], mimeType: mimeType, size: nil, attributes: [.FileName(fileName: name)]) - messages.append(.message(text: "", attributes: [], media: media, replyToMessageId: nil)) + messages.append(.message(text: caption ?? "", attributes: [], media: media, replyToMessageId: nil)) default: break } - case let .video(data, previewImage, adjustments): + case let .video(data, previewImage, adjustments, caption): var finalDimensions: CGSize var finalDuration: Double switch data { @@ -231,7 +231,7 @@ func legacyAssetPickerEnqueueMessages(account: Account, peerId: PeerId, signals: } let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: arc4random64()), resource: resource, previewRepresentations: previewRepresentations, mimeType: "video/mp4", size: nil, attributes: [.FileName(fileName: "video.mp4"), .Video(duration: Int(finalDuration), size: finalDimensions)]) - messages.append(.message(text: "", attributes: [], media: media, replyToMessageId: nil)) + messages.append(.message(text: caption ?? "", attributes: [], media: media, replyToMessageId: nil)) } } } diff --git a/TelegramUI/ListControllerButtonItem.swift b/TelegramUI/ListControllerButtonItem.swift index 19c8c8a0d3..8a362da5fe 100644 --- a/TelegramUI/ListControllerButtonItem.swift +++ b/TelegramUI/ListControllerButtonItem.swift @@ -43,7 +43,7 @@ class ListControllerButtonItemNode: ListControllerGroupableItemNode { let layoutLabel = TextNode.asyncLayout(self.label) return { item, width in if let item = item as? ListControllerButtonItem { - let (labelLayout, labelApply) = layoutLabel(NSAttributedString(string: item.title, font: titleFont, textColor: item.color), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (labelLayout, labelApply) = layoutLabel(NSAttributedString(string: item.title, font: titleFont, textColor: item.color), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) return (CGSize(width: width, height: 44.0), { [weak self] in if let strongSelf = self { let _ = labelApply() diff --git a/TelegramUI/ListControllerDisclosureActionItem.swift b/TelegramUI/ListControllerDisclosureActionItem.swift index ea9752303c..23b9fd6f3a 100644 --- a/TelegramUI/ListControllerDisclosureActionItem.swift +++ b/TelegramUI/ListControllerDisclosureActionItem.swift @@ -61,7 +61,7 @@ class ListControllerDisclosureActionItemNode: ListControllerGroupableItemNode { let layoutLabel = TextNode.asyncLayout(self.label) return { item, width in if let item = item as? ListControllerDisclosureActionItem { - let (labelLayout, labelApply) = layoutLabel(NSAttributedString(string: item.title, font: titleFont, textColor: UIColor.black), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (labelLayout, labelApply) = layoutLabel(NSAttributedString(string: item.title, font: titleFont, textColor: UIColor.black), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) return (CGSize(width: width, height: 44.0), { [weak self] in if let strongSelf = self { let _ = labelApply() diff --git a/TelegramUI/ListMessageFileItemNode.swift b/TelegramUI/ListMessageFileItemNode.swift index 05ece3214e..b396a76725 100644 --- a/TelegramUI/ListMessageFileItemNode.swift +++ b/TelegramUI/ListMessageFileItemNode.swift @@ -360,11 +360,11 @@ final class ListMessageFileItemNode: ListMessageNode { } } - let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(titleText, nil, 1, .middle, CGSize(width: width - leftInset - 8.0, height: CGFloat.infinity), .natural, nil) + let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(titleText, nil, 1, .middle, CGSize(width: width - leftInset - 8.0, height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) - let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(descriptionText, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - 12.0, height: CGFloat.infinity), .natural, nil) + let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(descriptionText, nil, 1, .end, CGSize(width: width - leftInset - 8.0 - 12.0, height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) - let (extensionTextLayout, extensionTextApply) = extensionIconTextMakeLayout(extensionText, nil, 1, .end, CGSize(width: 38.0, height: CGFloat.infinity), .natural, nil) + let (extensionTextLayout, extensionTextApply) = extensionIconTextMakeLayout(extensionText, nil, 1, .end, CGSize(width: 38.0, height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) var iconImageApply: (() -> Void)? if let iconImageRepresentation = iconImageRepresentation { diff --git a/TelegramUI/ListMessageSnippetItemNode.swift b/TelegramUI/ListMessageSnippetItemNode.swift index 0084a4507c..d2ad742ce5 100644 --- a/TelegramUI/ListMessageSnippetItemNode.swift +++ b/TelegramUI/ListMessageSnippetItemNode.swift @@ -147,11 +147,11 @@ final class ListMessageSnippetItemNode: ListMessageNode { } } - let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(title, nil, 1, .middle, CGSize(width: width - leftInset - 8.0, height: CGFloat.infinity), .natural, nil) + let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(title, nil, 1, .middle, CGSize(width: width - leftInset - 8.0, height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) - let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(descriptionText, nil, 0, .end, CGSize(width: width - leftInset - 8.0 - 12.0, height: CGFloat.infinity), .natural, nil) + let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(descriptionText, nil, 0, .end, CGSize(width: width - leftInset - 8.0 - 12.0, height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) - let (iconTextLayout, iconTextApply) = iconTextMakeLayout(iconText, nil, 1, .end, CGSize(width: 38.0, height: CGFloat.infinity), .natural, nil) + let (iconTextLayout, iconTextApply) = iconTextMakeLayout(iconText, nil, 1, .end, CGSize(width: 38.0, height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) var iconImageApply: (() -> Void)? if let iconImageRepresentation = iconImageRepresentation { diff --git a/TelegramUI/ListSectionHeaderNode.swift b/TelegramUI/ListSectionHeaderNode.swift index 0f0870940e..8313c67700 100644 --- a/TelegramUI/ListSectionHeaderNode.swift +++ b/TelegramUI/ListSectionHeaderNode.swift @@ -28,7 +28,7 @@ final class ListSectionHeaderNode: ASDisplayNode { let size = self.bounds.size let makeLayout = TextNode.asyncLayout(self.label) - let (labelLayout, labelApply) = makeLayout(NSAttributedString(string: self.title ?? "", font: Font.medium(12.0), textColor: UIColor(0x8e8e93)), self.backgroundColor, 1, .end, CGSize(width: max(0.0, size.width - 18.0), height: size.height), .natural, nil) + let (labelLayout, labelApply) = makeLayout(NSAttributedString(string: self.title ?? "", font: Font.medium(12.0), textColor: UIColor(0x8e8e93)), self.backgroundColor, 1, .end, CGSize(width: max(0.0, size.width - 18.0), height: size.height), .natural, nil, UIEdgeInsets()) let _ = labelApply() self.label.frame = CGRect(origin: CGPoint(x: 9.0, y: 6.0), size: labelLayout.size) } diff --git a/TelegramUI/ManagedAudioPlaylistPlayer.swift b/TelegramUI/ManagedAudioPlaylistPlayer.swift index 427d92e339..d87d1e162a 100644 --- a/TelegramUI/ManagedAudioPlaylistPlayer.swift +++ b/TelegramUI/ManagedAudioPlaylistPlayer.swift @@ -49,6 +49,7 @@ protocol AudioPlaylistItem { var id: AudioPlaylistItemId { get } var resource: MediaResource? { get } var info: AudioPlaylistItemInfo? { get } + var streamable: Bool { get } func isEqual(to: AudioPlaylistItem) -> Bool } @@ -126,6 +127,7 @@ private final class AudioPlaylistInternalState { } final class ManagedAudioPlaylistPlayer { + private let audioSessionManager: ManagedAudioSession private let postbox: Postbox let playlist: AudioPlaylist @@ -136,7 +138,8 @@ final class ManagedAudioPlaylistPlayer { return self.currentStateAndStatusValue.get() } - init(postbox: Postbox, playlist: AudioPlaylist) { + init(audioSessionManager: ManagedAudioSession, postbox: Postbox, playlist: AudioPlaylist) { + self.audioSessionManager = audioSessionManager self.postbox = postbox self.playlist = playlist } @@ -175,8 +178,26 @@ final class ManagedAudioPlaylistPlayer { if let strongSelf = self { let updatedStateAndStatus = strongSelf.currentState.with { state -> AudioPlaylistStateAndStatus in if let item = item { + if let item = item as? PeerMessageHistoryAudioPlaylistItem { + switch item.entry { + case let .MessageEntry(message, _, _, _): + if message.flags.contains(.Incoming) { + for attribute in message.attributes { + if let attribute = attribute as? ConsumableContentMessageAttribute { + if !attribute.consumed { + let _ = markMessageContentAsConsumedInteractively(postbox: strongSelf.postbox, messageId: message.id).start() + } + break + } + } + } + case .HoleEntry: + break + } + } + if let resource = item.resource { - let player = MediaPlayer(postbox: strongSelf.postbox, resource: resource) + let player = MediaPlayer(audioSessionManager: strongSelf.audioSessionManager, postbox: strongSelf.postbox, resource: resource, streamable: item.streamable) player.actionAtEnd = .action({ if let strongSelf = self { strongSelf.control(.navigation(.next)) diff --git a/TelegramUI/ManagedAudioRecorder.swift b/TelegramUI/ManagedAudioRecorder.swift index 59bb738c07..4d957ca1b7 100644 --- a/TelegramUI/ManagedAudioRecorder.swift +++ b/TelegramUI/ManagedAudioRecorder.swift @@ -56,7 +56,7 @@ private func removeAudioRecorderContext(_ id: Int32) { } private func addAudioUnitHolder(_ id: Int32, _ queue: Queue, _ audioUnit: Atomic) { - audioUnitHolders.modify { dict in + let _ = audioUnitHolders.modify { dict in var dict = dict dict[id] = AudioUnitHolder(queue: queue, audioUnit: audioUnit) return dict @@ -64,7 +64,7 @@ private func addAudioUnitHolder(_ id: Int32, _ queue: Queue, _ audioUnit: Atomic } private func removeAudioUnitHolder(_ id: Int32) { - audioUnitHolders.modify { dict in + let _ = audioUnitHolders.modify { dict in var dict = dict dict.removeValue(forKey: id) return dict @@ -93,7 +93,7 @@ private func withAudioUnitHolder(_ id: Int32, _ f: (Atomic, Queue) - } private func rendererInputProc(refCon: UnsafeMutableRawPointer, ioActionFlags: UnsafeMutablePointer, inTimeStamp: UnsafePointer, inBusNumber: UInt32, inNumberFrames: UInt32, ioData: UnsafeMutablePointer?) -> OSStatus { - let id = Int32(unsafeBitCast(refCon, to: intptr_t.self)) + let id = Int32(intptr_t(bitPattern: refCon)) withAudioUnitHolder(id, { (holder, queue) in var buffer = AudioBuffer() @@ -243,8 +243,6 @@ final class ManagedAudioRecorderContext { return } - var status = noErr - var one: UInt32 = 1 guard AudioUnitSetProperty(audioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, 1, &one, 4) == noErr else { AudioComponentInstanceDispose(audioUnit) @@ -264,7 +262,7 @@ final class ManagedAudioRecorderContext { var callbackStruct = AURenderCallbackStruct() callbackStruct.inputProc = rendererInputProc - callbackStruct.inputProcRefCon = unsafeBitCast(intptr_t(self.id), to: UnsafeMutableRawPointer.self) + callbackStruct.inputProcRefCon = UnsafeMutableRawPointer(bitPattern: intptr_t(self.id)) guard AudioUnitSetProperty(audioUnit, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Global, 0, &callbackStruct, UInt32(MemoryLayout.size)) == noErr else { AudioComponentInstanceDispose(audioUnit) return @@ -281,7 +279,7 @@ final class ManagedAudioRecorderContext { return } - self.audioUnit.swap(audioUnit) + let _ = self.audioUnit.swap(audioUnit) if self.audioSessionDisposable == nil { let queue = self.queue @@ -495,7 +493,7 @@ final class ManagedAudioRecorderContext { } for i in 0 ..< 100 { - var sample: UInt16 = UInt16(Int64(scaledSamples[i])) + let sample: UInt16 = UInt16(Int64(scaledSamples[i])) if sample > calculatedPeak { scaledSamples[i] = Int16(calculatedPeak) } diff --git a/TelegramUI/ManagedAudioSession.swift b/TelegramUI/ManagedAudioSession.swift index 74eec0fba7..b3de1ddb07 100644 --- a/TelegramUI/ManagedAudioSession.swift +++ b/TelegramUI/ManagedAudioSession.swift @@ -24,14 +24,16 @@ private final class HolderRecord { let audioSessionType: ManagedAudioSessionType let activate: () -> Void let deactivate: () -> Signal + let once: Bool var active: Bool = false var deactivatingDisposable: Disposable? = nil - init(id: Int32, audioSessionType: ManagedAudioSessionType, activate: @escaping () -> Void, deactivate: @escaping () -> Signal) { + init(id: Int32, audioSessionType: ManagedAudioSessionType, activate: @escaping () -> Void, deactivate: @escaping () -> Signal, once: Bool) { self.id = id self.audioSessionType = audioSessionType self.activate = activate self.deactivate = deactivate + self.once = once } } @@ -40,11 +42,16 @@ final class ManagedAudioSession { private let queue = Queue() private var holders: [HolderRecord] = [] private var currentType: ManagedAudioSessionType = .none + private var deactivateTimer: SwiftSignalKit.Timer? - func push(audioSessionType: ManagedAudioSessionType, activate: @escaping () -> Void, deactivate: @escaping () -> Signal) -> Disposable { + deinit { + self.deactivateTimer?.invalidate() + } + + func push(audioSessionType: ManagedAudioSessionType, activate: @escaping () -> Void, deactivate: @escaping () -> Signal, once: Bool = false) -> Disposable { let id = OSAtomicIncrement32(&self.nextId) self.queue.async { - self.holders.append(HolderRecord(id: id, audioSessionType: audioSessionType, activate: activate, deactivate: deactivate)) + self.holders.append(HolderRecord(id: id, audioSessionType: audioSessionType, activate: activate, deactivate: deactivate, once: once)) self.updateHolders() } return ActionDisposable { [weak self] in @@ -94,11 +101,16 @@ final class ManagedAudioSession { let id = self.holders[activeIndex].id self.holders[activeIndex].deactivatingDisposable = (self.holders[activeIndex].deactivate() |> deliverOn(self.queue)).start(completed: { [weak self] in if let strongSelf = self { + var index = 0 for currentRecord in strongSelf.holders { if currentRecord.id == id { currentRecord.deactivatingDisposable = nil + if currentRecord.once { + strongSelf.holders.remove(at: index) + } break } + index += 1 } strongSelf.updateHolders() } @@ -111,11 +123,27 @@ final class ManagedAudioSession { } } } else { - self.applyType(.none) + self.applyTypeNoneDelayed() } } + private func applyTypeNoneDelayed() { + self.deactivateTimer?.invalidate() + let deactivateTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { [weak self] in + if let strongSelf = self { + strongSelf.applyType(.none) + } + }, queue: self.queue) + self.deactivateTimer = deactivateTimer + deactivateTimer.start() + } + private func applyType(_ type: ManagedAudioSessionType) { + if type != .none { + self.deactivateTimer?.invalidate() + self.deactivateTimer = nil + } + if self.currentType != type { self.currentType = type diff --git a/TelegramUI/MediaManager.swift b/TelegramUI/MediaManager.swift index 524d26f0ff..5f24db2f59 100644 --- a/TelegramUI/MediaManager.swift +++ b/TelegramUI/MediaManager.swift @@ -4,6 +4,7 @@ import Postbox import AVFoundation import MobileCoreServices import TelegramCore +import MediaPlayer private struct WrappedAudioPlaylistItemId: Hashable, Equatable { let playlistId: AudioPlaylistId @@ -77,7 +78,7 @@ private final class ActiveManagedVideoContext { } } -final class MediaManager { +final class MediaManager: NSObject { private let queue = Queue.mainQueue() let audioSession = ManagedAudioSession() @@ -87,16 +88,120 @@ final class MediaManager { var playlistPlayerStateAndStatus: Signal { return self.playlistPlayerStateAndStatusValue.get() } - private var playlistPlayerStateValueDisposable: Disposable? + private let playlistPlayerStateValueDisposable = MetaDisposable() private let playlistPlayerStatusesContext = Atomic(value: ManagedAudioPlaylistPlayerStatusesContext()) + private let globalControlsStatus = Promise(nil) + + private let globalControlsDisposable = MetaDisposable() + private let globalControlsStatusDisposable = MetaDisposable() + private var managedVideoContexts: [WrappedManagedMediaId: ActiveManagedVideoContext] = [:] - init() { + override init() { + super.init() + + let commandCenter = MPRemoteCommandCenter.shared() + commandCenter.playCommand.addTarget(self, action: #selector(playCommandEvent(_:))) + commandCenter.pauseCommand.addTarget(self, action: #selector(pauseCommandEvent(_:))) + commandCenter.previousTrackCommand.addTarget(self, action: #selector(previousTrackCommandEvent(_:))) + commandCenter.nextTrackCommand.addTarget(self, action: #selector(nextTrackCommandEvent(_:))) + commandCenter.togglePlayPauseCommand.addTarget(self, action: #selector(togglePlayPauseCommandEvent(_:))) + if #available(iOSApplicationExtension 9.1, *) { + commandCenter.changePlaybackPositionCommand.addTarget(handler: { [weak self] event in + if let strongSelf = self, let event = event as? MPChangePlaybackPositionCommandEvent { + strongSelf.playlistPlayerControl(.playback(.seek(event.positionTime))) + } + return .success + }) + } + + var previousStateAndStatus: AudioPlaylistStateAndStatus? + let globalControlsStatus = self.globalControlsStatus + + var baseNowPlayingInfo: [String: Any]? + + self.globalControlsDisposable.set((self.playlistPlayerStateAndStatusValue.get() |> deliverOnMainQueue).start(next: { next in + if let next = next, let item = next.state.item, let info = item.info { + let commandCenter = MPRemoteCommandCenter.shared() + commandCenter.playCommand.isEnabled = true + commandCenter.pauseCommand.isEnabled = true + commandCenter.previousTrackCommand.isEnabled = true + commandCenter.nextTrackCommand.isEnabled = true + commandCenter.togglePlayPauseCommand.isEnabled = true + + var nowPlayingInfo: [String: Any] = [:] + + switch info.labelInfo { + case let .music(title, performer): + let titleText: String = title ?? "Unknown Track" + let subtitleText: String = performer ?? "Unknown Artist" + + nowPlayingInfo[MPMediaItemPropertyTitle] = titleText + nowPlayingInfo[MPMediaItemPropertyArtist] = subtitleText + case .voice: + let titleText: String = "Voice Message" + + nowPlayingInfo[MPMediaItemPropertyTitle] = titleText + } + + baseNowPlayingInfo = nowPlayingInfo + + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + + if previousStateAndStatus != next { + previousStateAndStatus = next + if let status = next.status { + globalControlsStatus.set(status |> map { Optional($0) }) + } else { + globalControlsStatus.set(.single(nil)) + } + } + } else { + previousStateAndStatus = nil + baseNowPlayingInfo = nil + globalControlsStatus.set(.single(nil)) + + let commandCenter = MPRemoteCommandCenter.shared() + commandCenter.playCommand.isEnabled = false + commandCenter.pauseCommand.isEnabled = false + commandCenter.previousTrackCommand.isEnabled = false + commandCenter.nextTrackCommand.isEnabled = false + commandCenter.togglePlayPauseCommand.isEnabled = false + + MPNowPlayingInfoCenter.default().nowPlayingInfo = nil + } + })) + + self.globalControlsStatusDisposable.set((self.globalControlsStatus.get() |> deliverOnMainQueue).start(next: { next in + if let next = next { + if var nowPlayingInfo = baseNowPlayingInfo { + nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = next.duration as NSNumber + switch next.status { + case .playing: + nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 1.0 as NSNumber + case .buffering, .paused: + nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 0.0 as NSNumber + } + nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = next.timestamp as NSNumber + + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + } + }/* else { + if var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo { + nowPlayingInfo.removeValue(forKey: MPMediaItemPropertyPlaybackDuration) + nowPlayingInfo.removeValue(forKey: MPNowPlayingInfoPropertyPlaybackRate) + nowPlayingInfo.removeValue(forKey: MPNowPlayingInfoPropertyElapsedPlaybackTime) + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + } + }*/ + })) } deinit { - self.playlistPlayerStateValueDisposable?.dispose() + self.playlistPlayerStateValueDisposable.dispose() + self.globalControlsDisposable.dispose() + self.globalControlsStatusDisposable.dispose() } func videoContext(account: Account, id: ManagedMediaId, resource: MediaResource) -> Signal { @@ -109,7 +214,7 @@ final class MediaManager { if let currentActiveContext = self.managedVideoContexts[wrappedId] { activeContext = currentActiveContext } else { - let mediaPlayer = MediaPlayer(postbox: account.postbox, resource: resource) + let mediaPlayer = MediaPlayer(audioSessionManager: self.audioSession, postbox: account.postbox, resource: resource, streamable: false) let playerNode = MediaPlayerNode() mediaPlayer.attachPlayerNode(playerNode) activeContext = ActiveManagedVideoContext(context: ManagedVideoContext(mediaPlayer: mediaPlayer, playerNode: playerNode)) @@ -167,7 +272,7 @@ final class MediaManager { func setPlaylistPlayer(_ player: ManagedAudioPlaylistPlayer?) { var disposePlayer: ManagedAudioPlaylistPlayer? var updatedPlayer = false - self.playlistPlayer.modify { currentPlayer in + let _ = self.playlistPlayer.modify { currentPlayer in if currentPlayer !== player { disposePlayer = currentPlayer updatedPlayer = true @@ -178,17 +283,33 @@ final class MediaManager { } if let disposePlayer = disposePlayer { + withExtendedLifetime(disposePlayer, { + + }) } if updatedPlayer { if let player = player { self.playlistPlayerStateAndStatusValue.set(player.stateAndStatus) + self.playlistPlayerStateValueDisposable.set(player.stateAndStatus.start(next: { [weak self] next in + if let next = next { + if next.state.item == nil { + Queue.mainQueue().async { + self?.setPlaylistPlayer(nil) + } + } + } + })) } else { self.playlistPlayerStateAndStatusValue.set(.single(nil)) } } } + private func updatePlaylistPlayerStateValue() { + + } + func playlistPlayerControl(_ control: AudioPlaylistControl) { var player: ManagedAudioPlaylistPlayer? self.playlistPlayer.with { currentPlayer -> Void in @@ -227,4 +348,24 @@ final class MediaManager { } }*/ } + + @objc func playCommandEvent(_ command: AnyObject) { + self.playlistPlayerControl(.playback(.play)) + } + + @objc func pauseCommandEvent(_ command: AnyObject) { + self.playlistPlayerControl(.playback(.pause)) + } + + @objc func previousTrackCommandEvent(_ command: AnyObject) { + self.playlistPlayerControl(.navigation(.previous)) + } + + @objc func nextTrackCommandEvent(_ command: AnyObject) { + self.playlistPlayerControl(.navigation(.next)) + } + + @objc func togglePlayPauseCommandEvent(_ command: AnyObject) { + self.playlistPlayerControl(.playback(.togglePlayPause)) + } } diff --git a/TelegramUI/MediaNavigationAccessoryContainerNode.swift b/TelegramUI/MediaNavigationAccessoryContainerNode.swift index b67e210584..f96a550238 100644 --- a/TelegramUI/MediaNavigationAccessoryContainerNode.swift +++ b/TelegramUI/MediaNavigationAccessoryContainerNode.swift @@ -88,7 +88,7 @@ final class MediaNavigationAccessoryContainerNode: ASDisplayNode, UIGestureRecog case .began: self.draggingHeaderHeight = self.currentHeaderHeight case .changed: - if let draggingHeaderHeight = self.draggingHeaderHeight { + if let _ = self.draggingHeaderHeight { let translation = recognizer.translation(in: self.view).y self.draggingHeaderHeight = max(MediaNavigationAccessoryHeaderNode.minimizedHeight, self.currentHeaderHeight + translation) self.updateLayout(size: self.bounds.size, transition: .immediate) diff --git a/TelegramUI/MediaNavigationAccessoryHeaderNode.swift b/TelegramUI/MediaNavigationAccessoryHeaderNode.swift index 3660196f69..afe3bd838c 100644 --- a/TelegramUI/MediaNavigationAccessoryHeaderNode.swift +++ b/TelegramUI/MediaNavigationAccessoryHeaderNode.swift @@ -116,8 +116,8 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode { self.actionPlayNode.image = playIcon self.actionPlayNode.isHidden = true - self.maximizedLeftTimestampNode = MediaPlayerTimeTextNode() - self.maximizedRightTimestampNode = MediaPlayerTimeTextNode() + self.maximizedLeftTimestampNode = MediaPlayerTimeTextNode(textColor: UIColor(0x686669)) + self.maximizedRightTimestampNode = MediaPlayerTimeTextNode(textColor: UIColor(0x686669)) self.maximizedLeftTimestampNode.alignment = .right self.maximizedRightTimestampNode.mode = .reversed @@ -292,32 +292,38 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode { let makeMaximizedTitleLayout = TextNode.asyncLayout(self.maximizedTitleNode) let makeMaximizedSubtitleLayout = TextNode.asyncLayout(self.maximizedSubtitleNode) - let (titleLayout, titleApply) = makeTitleLayout(titleString, nil, 1, .middle, CGSize(width: size.width - 80.0, height: 100.0), .natural, nil) - let (subtitleLayout, subtitleApply) = makeSubtitleLayout(subtitleString, nil, 1, .middle, CGSize(width: size.width - 80.0, height: 100.0), .natural, nil) + let (titleLayout, titleApply) = makeTitleLayout(titleString, nil, 1, .middle, CGSize(width: size.width - 80.0, height: 100.0), .natural, nil, UIEdgeInsets()) + let (subtitleLayout, subtitleApply) = makeSubtitleLayout(subtitleString, nil, 1, .middle, CGSize(width: size.width - 80.0, height: 100.0), .natural, nil, UIEdgeInsets()) - let (maximizedTitleLayout, maximizedTitleApply) = makeMaximizedTitleLayout(maximizedTitleString, nil, 1, .middle, CGSize(width: size.width - 80.0, height: 100.0), .natural, nil) - let (maximizedSubtitleLayout, maximizedSubtitleApply) = makeMaximizedSubtitleLayout(maximizedSubtitleString, nil, 1, .middle, CGSize(width: size.width - 80.0, height: 100.0), .natural, nil) + let (maximizedTitleLayout, maximizedTitleApply) = makeMaximizedTitleLayout(maximizedTitleString, nil, 1, .middle, CGSize(width: size.width - 80.0, height: 100.0), .natural, nil, UIEdgeInsets()) + let (maximizedSubtitleLayout, maximizedSubtitleApply) = makeMaximizedSubtitleLayout(maximizedSubtitleString, nil, 1, .middle, CGSize(width: size.width - 80.0, height: 100.0), .natural, nil, UIEdgeInsets()) - titleApply() - subtitleApply() - maximizedTitleApply() - maximizedSubtitleApply() + let _ = titleApply() + let _ = subtitleApply() + let _ = maximizedTitleApply() + let _ = maximizedSubtitleApply() - let minimizedTitleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleLayout.size.width) / 2.0), y: 4.0), size: titleLayout.size) + let minimizedTitleOffset: CGFloat = subtitleString == nil ? 6.0 : 0.0 + let maximizedTitleOffset: CGFloat = subtitleString == nil ? 12.0 : 0.0 + + let minimizedTitleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleLayout.size.width) / 2.0), y: 4.0 + minimizedTitleOffset), size: titleLayout.size) let minimizedSubtitleFrame = CGRect(origin: CGPoint(x: floor((size.width - subtitleLayout.size.width) / 2.0), y: 20.0), size: subtitleLayout.size) - let maximizedTitleFrame = CGRect(origin: CGPoint(x: floor((size.width - maximizedTitleLayout.size.width) / 2.0), y: 57.0), size: maximizedTitleLayout.size) + let maximizedTitleFrame = CGRect(origin: CGPoint(x: floor((size.width - maximizedTitleLayout.size.width) / 2.0), y: 57.0 + maximizedTitleOffset), size: maximizedTitleLayout.size) let maximizedSubtitleFrame = CGRect(origin: CGPoint(x: floor((size.width - maximizedSubtitleLayout.size.width) / 2.0), y: 80.0), size: maximizedSubtitleLayout.size) let maximizedTitleDistance = maximizedTitleFrame.midY - minimizedTitleFrame.midY let maximizedSubtitleDistance = maximizedSubtitleFrame.midY - minimizedSubtitleFrame.midY - let updatedMinimizedTitleFrame = minimizedTitleFrame.offsetBy(dx: 0.0, dy: maximizedTitleDistance * maximizationFactor) - let updatedMaximizedTitleFrame = maximizedTitleFrame.offsetBy(dx: 0.0, dy: -maximizedTitleDistance * (1.0 - maximizationFactor)) + var updatedMinimizedTitleFrame = minimizedTitleFrame.offsetBy(dx: 0.0, dy: maximizedTitleDistance * maximizationFactor) + var updatedMaximizedTitleFrame = maximizedTitleFrame.offsetBy(dx: 0.0, dy: -maximizedTitleDistance * (1.0 - maximizationFactor)) transition.updateFrame(node: self.titleNode, frame: updatedMinimizedTitleFrame) transition.updateFrame(node: self.subtitleNode, frame: minimizedSubtitleFrame.offsetBy(dx: 0.0, dy: maximizedSubtitleDistance * maximizationFactor)) + updatedMinimizedTitleFrame.origin.y -= minimizedTitleOffset + updatedMaximizedTitleFrame.origin.y -= maximizedTitleOffset + transition.updateFrame(node: self.maximizedTitleNode, frame: updatedMaximizedTitleFrame) transition.updateFrame(node: self.maximizedSubtitleNode, frame: maximizedSubtitleFrame.offsetBy(dx: 0.0, dy: -maximizedSubtitleDistance * (1.0 - maximizationFactor))) diff --git a/TelegramUI/MediaNavigationAccessoryItemListNode.swift b/TelegramUI/MediaNavigationAccessoryItemListNode.swift index 4871b6d772..4b462eaf8a 100644 --- a/TelegramUI/MediaNavigationAccessoryItemListNode.swift +++ b/TelegramUI/MediaNavigationAccessoryItemListNode.swift @@ -57,7 +57,7 @@ final class MediaNavigationAccessoryItemListNode: ASDisplayNode { if let galleryMedia = galleryMedia { if let file = galleryMedia as? TelegramMediaFile, file.isMusic || file.isVoice { if let applicationContext = strongSelf.account.applicationContext as? TelegramApplicationContext { - let player = ManagedAudioPlaylistPlayer(postbox: strongSelf.account.postbox, playlist: peerMessageHistoryAudioPlaylist(account: strongSelf.account, messageId: id)) + let player = ManagedAudioPlaylistPlayer(audioSessionManager: (strongSelf.account.applicationContext as! TelegramApplicationContext).mediaManager.audioSession, postbox: strongSelf.account.postbox, playlist: peerMessageHistoryAudioPlaylist(account: strongSelf.account, messageId: id)) applicationContext.mediaManager.setPlaylistPlayer(player) player.control(.navigation(.next)) } diff --git a/TelegramUI/MediaPlayer.swift b/TelegramUI/MediaPlayer.swift index 3bcf558ec3..3d4b817249 100644 --- a/TelegramUI/MediaPlayer.swift +++ b/TelegramUI/MediaPlayer.swift @@ -36,8 +36,10 @@ enum MediaPlayerActionAtEnd { private final class MediaPlayerContext { private let queue: Queue + private let audioSessionManager: ManagedAudioSession private let postbox: Postbox private let resource: MediaResource + private let streamable: Bool private var state: MediaPlayerState = .empty private var audioRenderer: MediaPlayerAudioRenderer? @@ -71,13 +73,15 @@ private final class MediaPlayerContext { } } - init(queue: Queue, playerStatus: ValuePromise, postbox: Postbox, resource: MediaResource) { + init(queue: Queue, audioSessionManager: ManagedAudioSession, playerStatus: ValuePromise, postbox: Postbox, resource: MediaResource, streamable: Bool) { assert(queue.isCurrent()) self.queue = queue + self.audioSessionManager = audioSessionManager self.playerStatus = playerStatus self.postbox = postbox self.resource = resource + self.streamable = streamable } deinit { @@ -126,7 +130,7 @@ private final class MediaPlayerContext { self.tickTimer?.invalidate() if let loadedState = loadedState { if loadedState.controlTimebase.isAudio { - self.audioRenderer?.rate = 0.0 + self.audioRenderer?.setRate(0.0) } else { if !CMTimebaseGetRate(loadedState.controlTimebase.timebase).isEqual(to: 0.0) { CMTimebaseSetRate(loadedState.controlTimebase.timebase, 0.0) @@ -152,7 +156,7 @@ private final class MediaPlayerContext { self.playerStatus.set(status) } - let frameSource = FFMpegMediaFrameSource(queue: self.queue, postbox: self.postbox, resource: resource) + let frameSource = FFMpegMediaFrameSource(queue: self.queue, postbox: self.postbox, resource: self.resource, streamable: self.streamable) let disposable = MetaDisposable() self.state = .seeking(frameSource: frameSource, timestamp: timestamp, disposable: disposable, action: action) @@ -189,8 +193,16 @@ private final class MediaPlayerContext { if let currentRenderer = self.audioRenderer { renderer = currentRenderer } else { - renderer = MediaPlayerAudioRenderer() + let queue = self.queue + renderer = MediaPlayerAudioRenderer(audioSessionManager: self.audioSessionManager, audioPaused: { [weak self] in + queue.async { + if let strongSelf = self { + strongSelf.pause() + } + } + }) self.audioRenderer = renderer + renderer.start() } controlTimebase = MediaPlayerControlTimebase(timebase: renderer.audioTimebase, isAudio: true) @@ -447,7 +459,7 @@ private final class MediaPlayerContext { } if loadedState.controlTimebase.isAudio { - self.audioRenderer?.rate = rate + self.audioRenderer?.setRate(rate) } else { if !CMTimebaseGetRate(loadedState.controlTimebase.timebase).isEqual(to: rate) { CMTimebaseSetRate(loadedState.controlTimebase.timebase, rate) @@ -582,9 +594,9 @@ final class MediaPlayer { } } - init(postbox: Postbox, resource: MediaResource) { + init(audioSessionManager: ManagedAudioSession, postbox: Postbox, resource: MediaResource, streamable: Bool) { self.queue.async { - let context = MediaPlayerContext(queue: self.queue, playerStatus: self.statusValue, postbox: postbox, resource: resource) + let context = MediaPlayerContext(queue: self.queue, audioSessionManager: audioSessionManager, playerStatus: self.statusValue, postbox: postbox, resource: resource, streamable: streamable) self.contextRef = Unmanaged.passRetained(context) } } diff --git a/TelegramUI/MediaPlayerAudioRenderer.swift b/TelegramUI/MediaPlayerAudioRenderer.swift index 926a820bf6..4b362d4f41 100644 --- a/TelegramUI/MediaPlayerAudioRenderer.swift +++ b/TelegramUI/MediaPlayerAudioRenderer.swift @@ -76,7 +76,7 @@ private func rendererInputProc(refCon: UnsafeMutableRawPointer, ioActionFlags: U var rendererFillOffset = (0, 0) var notifyLowWater: (() -> Void)? - withPlayerRendererBuffer(Int32(unsafeBitCast(refCon, to: intptr_t.self)), { context in + withPlayerRendererBuffer(Int32(intptr_t(bitPattern: refCon)), { context in context.with { context in switch context.state { case let .playing(didSetRate): @@ -169,8 +169,11 @@ private final class AudioPlayerRendererContext { let bufferSizeInSeconds: Int = 5 let lowWaterSizeInSeconds: Int = 2 + let audioSessionManager: ManagedAudioSession let controlTimebase: CMTimebase + let audioPaused: () -> Void + var paused = true var audioUnit: AudioComponentInstance? var bufferContextId: Int32! @@ -178,10 +181,14 @@ private final class AudioPlayerRendererContext { var requestingFramesContext: RequestingFramesContext? - init(controlTimebase: CMTimebase) { + let audioSessionDisposable = MetaDisposable() + + init(controlTimebase: CMTimebase, audioSessionManager: ManagedAudioSession, audioPaused: @escaping () -> Void) { assert(audioPlayerRendererQueue.isCurrent()) + self.audioSessionManager = audioSessionManager self.controlTimebase = controlTimebase + self.audioPaused = audioPaused self.audioStreamDescription = audioRendererNativeStreamDescription() @@ -207,6 +214,8 @@ private final class AudioPlayerRendererContext { deinit { assert(audioPlayerRendererQueue.isCurrent()) + self.audioSessionDisposable.dispose() + unregisterPlayerRendererBufferContext(self.bufferContextId) self.closeAudioUnit() @@ -215,6 +224,10 @@ private final class AudioPlayerRendererContext { fileprivate func setPlaying(_ playing: Bool) { assert(audioPlayerRendererQueue.isCurrent()) + if playing && self.paused { + self.start() + } + self.bufferContext.with { context in if playing { context.state = .playing(didSetRate: false) @@ -247,15 +260,23 @@ private final class AudioPlayerRendererContext { } } - fileprivate func startAudioUnit() { + fileprivate func start() { + if self.paused { + self.paused = false + self.startAudioUnit() + } + } + + fileprivate func stop() { + if !self.paused { + self.paused = true + self.setPlaying(false) + self.closeAudioUnit() + } + } + + private func startAudioUnit() { if self.audioUnit == nil { - guard let _ = try? AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback) else { - return - } - guard let _ = try? AVAudioSession.sharedInstance().setActive(true) else { - return - } - var desc = AudioComponentDescription() desc.componentType = kAudioUnitType_Output desc.componentSubType = kAudioUnitSubType_RemoteIO @@ -290,7 +311,7 @@ private final class AudioPlayerRendererContext { var callbackStruct = AURenderCallbackStruct() callbackStruct.inputProc = rendererInputProc - callbackStruct.inputProcRefCon = unsafeBitCast(intptr_t(self.bufferContextId), to: UnsafeMutableRawPointer.self) + callbackStruct.inputProcRefCon = UnsafeMutableRawPointer(bitPattern: intptr_t(self.bufferContextId)) guard AudioUnitSetProperty(audioUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Global, kOutputBus, &callbackStruct, UInt32(MemoryLayout.size)) == noErr else { AudioComponentInstanceDispose(audioUnit) return @@ -301,16 +322,41 @@ private final class AudioPlayerRendererContext { return } + self.audioUnit = audioUnit + } + + self.audioSessionDisposable.set(self.audioSessionManager.push(audioSessionType: .play, activate: { [weak self] in + audioPlayerRendererQueue.async { + if let strongSelf = self, !strongSelf.paused { + strongSelf.audioSessionAcquired() + } + } + }, deactivate: { [weak self] in + return Signal { subscriber in + audioPlayerRendererQueue.async { + if let strongSelf = self { + strongSelf.audioPaused() + strongSelf.stop() + subscriber.putCompletion() + } + } + + return EmptyDisposable + } + }, once: true)) + } + + private func audioSessionAcquired() { + if let audioUnit = self.audioUnit { guard AudioOutputUnitStart(audioUnit) == noErr else { - AudioComponentInstanceDispose(audioUnit) + self.closeAudioUnit() return } - self.audioUnit = audioUnit } } - fileprivate func closeAudioUnit() { + private func closeAudioUnit() { assert(audioPlayerRendererQueue.isCurrent()) if let audioUnit = self.audioUnit { @@ -359,10 +405,10 @@ private final class AudioPlayerRendererContext { if takeLength == context.overflowData.count { let data = context.overflowData context.overflowData = Data() - self.enqueueSamples(data, sampleIndex: context.overflowDataMaxChannelSampleIndex - (data.count / (2 * 2))) + self.enqueueSamples(data, sampleIndex: context.overflowDataMaxChannelSampleIndex - Int64(data.count / (2 * 2))) } else { let data = context.overflowData.subdata(in: 0 ..< takeLength) - self.enqueueSamples(data, sampleIndex: context.overflowDataMaxChannelSampleIndex - (context.overflowData.count / (2 * 2))) + self.enqueueSamples(data, sampleIndex: context.overflowDataMaxChannelSampleIndex - Int64(context.overflowData.count / (2 * 2))) context.overflowData.replaceSubrange(0 ..< takeLength, with: Data()) } } @@ -429,7 +475,7 @@ private final class AudioPlayerRendererContext { let bytesToCopy = min(context.buffer.size - context.buffer.availableBytes, data.count) data.withUnsafeBytes { (bytes: UnsafePointer) -> Void in let _ = context.buffer.enqueue(UnsafeRawPointer(bytes), count: bytesToCopy) - context.bufferMaxChannelSampleIndex = sampleIndex + (data.count / (2 * 2)) + context.bufferMaxChannelSampleIndex = sampleIndex + Int64(data.count / (2 * 2)) } } } @@ -468,24 +514,7 @@ final class MediaPlayerAudioRenderer { private let audioClock: CMClock let audioTimebase: CMTimebase - var rate: Double = 0.0 { - didSet { - let rate = self.rate - if !oldValue.isEqual(to: rate) { - print("setting audio rate to \(rate)") - assert(rate.isEqual(to: 1.0) || rate.isEqual(to: 0.0)) - - audioPlayerRendererQueue.async { - if let contextRef = self.contextRef { - let context = contextRef.takeUnretainedValue() - context.setPlaying(rate.isEqual(to: 1.0)) - } - } - } - } - } - - init() { + init(audioSessionManager: ManagedAudioSession, audioPaused: @escaping () -> Void) { var audioClock: CMClock? CMAudioClockCreate(nil, &audioClock) self.audioClock = audioClock! @@ -495,7 +524,7 @@ final class MediaPlayerAudioRenderer { self.audioTimebase = audioTimebase! audioPlayerRendererQueue.async { - let context = AudioPlayerRendererContext(controlTimebase: audioTimebase!) + let context = AudioPlayerRendererContext(controlTimebase: audioTimebase!, audioSessionManager: audioSessionManager, audioPaused: audioPaused) self.contextRef = Unmanaged.passRetained(context) } } @@ -511,7 +540,7 @@ final class MediaPlayerAudioRenderer { audioPlayerRendererQueue.async { if let contextRef = self.contextRef { let context = contextRef.takeUnretainedValue() - context.startAudioUnit() + context.start() } } } @@ -520,7 +549,16 @@ final class MediaPlayerAudioRenderer { audioPlayerRendererQueue.async { if let contextRef = self.contextRef { let context = contextRef.takeUnretainedValue() - context.closeAudioUnit() + context.stop() + } + } + } + + func setRate(_ rate: Double) { + audioPlayerRendererQueue.async { + if let contextRef = self.contextRef { + let context = contextRef.takeUnretainedValue() + context.setPlaying(rate.isEqual(to: 1.0)) } } } diff --git a/TelegramUI/MediaPlayerScrubbingNode.swift b/TelegramUI/MediaPlayerScrubbingNode.swift index b5e73b96dd..8cb56654a2 100644 --- a/TelegramUI/MediaPlayerScrubbingNode.swift +++ b/TelegramUI/MediaPlayerScrubbingNode.swift @@ -236,8 +236,8 @@ final class MediaPlayerScrubbingNode: ASDisplayNode { animation.fromValue = from animation.toValue = to animation.duration = duration - animation.isRemovedOnCompletion = true - animation.fillMode = kCAFillModeBackwards + //animation.isRemovedOnCompletion = true + animation.fillMode = kCAFillModeBoth animation.speed = speed animation.timeOffset = offset animation.isAdditive = false diff --git a/TelegramUI/MediaPlayerTimeTextNode.swift b/TelegramUI/MediaPlayerTimeTextNode.swift index 1e3e2b2184..e4ffa880b3 100644 --- a/TelegramUI/MediaPlayerTimeTextNode.swift +++ b/TelegramUI/MediaPlayerTimeTextNode.swift @@ -11,14 +11,14 @@ enum MediaPlayerTimeTextNodeMode { } private struct MediaPlayerTimeTextNodeState: Equatable { - let hours: Int32 - let minutes: Int32 - let seconds: Int32 + let hours: Int32? + let minutes: Int32? + let seconds: Int32? init() { - self.hours = 0 - self.minutes = 0 - self.seconds = 0 + self.hours = nil + self.minutes = nil + self.seconds = nil } init(hours: Int32, minutes: Int32, seconds: Int32) { @@ -39,11 +39,14 @@ private final class MediaPlayerTimeTextNodeParameters: NSObject { let state: MediaPlayerTimeTextNodeState let alignment: NSTextAlignment let mode: MediaPlayerTimeTextNodeMode + let textColor: UIColor - init(state: MediaPlayerTimeTextNodeState, alignment: NSTextAlignment, mode: MediaPlayerTimeTextNodeMode) { + init(state: MediaPlayerTimeTextNodeState, alignment: NSTextAlignment, mode: MediaPlayerTimeTextNodeMode, textColor: UIColor) { self.state = state self.alignment = alignment self.mode = mode + self.textColor = textColor + super.init() } } @@ -51,6 +54,7 @@ private final class MediaPlayerTimeTextNodeParameters: NSObject { final class MediaPlayerTimeTextNode: ASDisplayNode { var alignment: NSTextAlignment = .left var mode: MediaPlayerTimeTextNodeMode = .normal + private let textColor: UIColor private var statusValue: MediaPlayerStatus? { didSet { @@ -81,8 +85,11 @@ final class MediaPlayerTimeTextNode: ASDisplayNode { } } - override init() { + init(textColor: UIColor) { + self.textColor = textColor + super.init() + self.isOpaque = false self.statusDisposable = (self.statusValuePromise.get() @@ -113,7 +120,7 @@ final class MediaPlayerTimeTextNode: ASDisplayNode { } override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { - return MediaPlayerTimeTextNodeParameters(state: self.state, alignment: self.alignment, mode: self.mode) + return MediaPlayerTimeTextNodeParameters(state: self.state, alignment: self.alignment, mode: self.mode, textColor: self.textColor) } @objc override public class func draw(_ bounds: CGRect, withParameters parameters: NSObjectProtocol?, isCancelled: () -> Bool, isRasterizing: Bool) { @@ -126,8 +133,13 @@ final class MediaPlayerTimeTextNode: ASDisplayNode { } if let parameters = parameters as? MediaPlayerTimeTextNodeParameters { - let text = String(format: "%d:%02d", parameters.state.minutes, parameters.state.seconds) - let string = NSAttributedString(string: text, font: textFont, textColor: UIColor(0x686669)) + let text: String + if let minutes = parameters.state.minutes, let seconds = parameters.state.seconds { + text = String(format: "%d:%02d", minutes, seconds) + } else { + text = "-:--" + } + let string = NSAttributedString(string: text, font: textFont, textColor: parameters.textColor) let size = string.boundingRect(with: CGSize(width: 200.0, height: 100.0), options: NSStringDrawingOptions.usesLineFragmentOrigin, context: nil).size if parameters.alignment == .left { diff --git a/TelegramUI/MediaResources.swift b/TelegramUI/MediaResources.swift index d2763ada66..a115e7af81 100644 --- a/TelegramUI/MediaResources.swift +++ b/TelegramUI/MediaResources.swift @@ -154,3 +154,52 @@ public final class LocalFileVideoMediaResource: TelegramMediaResource { } } } + +public struct PhotoLibraryMediaResourceId: MediaResourceId { + public let localIdentifier: String + + public var uniqueId: String { + return "ph-\(self.localIdentifier.replacingOccurrences(of: "/", with: "_"))" + } + + public var hashValue: Int { + return self.localIdentifier.hashValue + } + + public func isEqual(to: MediaResourceId) -> Bool { + if let to = to as? PhotoLibraryMediaResourceId { + return self.localIdentifier == to.localIdentifier + } else { + return false + } + } +} + +public class PhotoLibraryMediaResource: TelegramMediaResource { + let localIdentifier: String + + public init(localIdentifier: String) { + self.localIdentifier = localIdentifier + } + + public required init(decoder: Decoder) { + self.localIdentifier = decoder.decodeStringForKey("i") + } + + public func encode(_ encoder: Encoder) { + encoder.encodeString(self.localIdentifier, forKey: "i") + } + + public var id: MediaResourceId { + return PhotoLibraryMediaResourceId(localIdentifier: self.localIdentifier) + } + + public func isEqual(to: TelegramMediaResource) -> Bool { + if let to = to as? PhotoLibraryMediaResource { + return self.localIdentifier == to.localIdentifier + } else { + return false + } + } +} + diff --git a/TelegramUI/MentionChatInputPanelItem.swift b/TelegramUI/MentionChatInputPanelItem.swift index 2b76d20c0f..e6adcfd9a8 100644 --- a/TelegramUI/MentionChatInputPanelItem.swift +++ b/TelegramUI/MentionChatInputPanelItem.swift @@ -124,7 +124,7 @@ final class MentionChatInputPanelItemNode: ListViewItemNode { let leftInset: CGFloat = 55.0 let rightInset: CGFloat = 10.0 - let (textLayout, textApply) = makeTextLayout(NSAttributedString(string: item.peer.displayTitle, font: textFont, textColor: .black), nil, 1, .end, CGSize(width: width - leftInset - rightInset, height: 100.0), .natural, nil) + let (textLayout, textApply) = makeTextLayout(NSAttributedString(string: item.peer.displayTitle, font: textFont, textColor: .black), nil, 1, .end, CGSize(width: width - leftInset - rightInset, height: 100.0), .natural, nil, UIEdgeInsets()) let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: HashtagChatInputPanelItemNode.itemHeight), insets: UIEdgeInsets()) diff --git a/TelegramUI/PeerMediaAudioPlaylist.swift b/TelegramUI/PeerMediaAudioPlaylist.swift index e12af4a947..49fe73110b 100644 --- a/TelegramUI/PeerMediaAudioPlaylist.swift +++ b/TelegramUI/PeerMediaAudioPlaylist.swift @@ -19,7 +19,7 @@ struct PeerMessageHistoryAudioPlaylistItemId: AudioPlaylistItemId { } } -private final class PeerMessageHistoryAudioPlaylistItem: AudioPlaylistItem { +final class PeerMessageHistoryAudioPlaylistItem: AudioPlaylistItem { let entry: MessageHistoryEntry var id: AudioPlaylistItemId { @@ -28,7 +28,7 @@ private final class PeerMessageHistoryAudioPlaylistItem: AudioPlaylistItem { var resource: MediaResource? { switch self.entry { - case let .MessageEntry(message, _, _): + case let .MessageEntry(message, _, _, _): for media in message.media { if let file = media as? TelegramMediaFile { return file.resource @@ -40,13 +40,29 @@ private final class PeerMessageHistoryAudioPlaylistItem: AudioPlaylistItem { } } + var streamable: Bool { + switch self.entry { + case let .MessageEntry(message, _, _, _): + for media in message.media { + if let file = media as? TelegramMediaFile { + if file.isMusic { + return true + } + } + } + return false + case .HoleEntry: + return false + } + } + var info: AudioPlaylistItemInfo? { switch self.entry { - case let .MessageEntry(message, _, _): + case let .MessageEntry(message, _, _, _): for media in message.media { if let file = media as? TelegramMediaFile { for attribute in file.attributes { - if case let .Audio(isVoice, duration, title, performer, waveform: nil) = attribute { + if case let .Audio(isVoice, duration, title, performer, _) = attribute { if isVoice { return AudioPlaylistItemInfo(duration: Double(duration), labelInfo: .voice) } else { @@ -99,40 +115,60 @@ func peerMessageAudioPlaylistAndItemIds(_ message: Message) -> (AudioPlaylistId, func peerMessageHistoryAudioPlaylist(account: Account, messageId: MessageId) -> AudioPlaylist { return AudioPlaylist(id: PeerMessageHistoryAudioPlaylistId(peerId: messageId.peerId), navigate: { item, navigation in if let item = item as? PeerMessageHistoryAudioPlaylistItem { - return account.postbox.aroundMessageHistoryViewForPeerId(item.entry.index.id.peerId, index: item.entry.index, count: 10, anchorIndex: item.entry.index, fixedCombinedReadState: nil, topTaggedMessageIdNamespaces: [], tagMask: .Music) - |> take(1) - |> map { (view, _, _) -> AudioPlaylistItem? in - var index = 0 - for entry in view.entries { - if entry.index.id == item.entry.index.id { - switch navigation { - case .previous: - if index + 1 < view.entries.count { - return PeerMessageHistoryAudioPlaylistItem(entry: view.entries[index + 1]) - } else { - return PeerMessageHistoryAudioPlaylistItem(entry: view.entries.last!) - } - case .next: - if index != 0 { - return PeerMessageHistoryAudioPlaylistItem(entry: view.entries[index - 1]) - } else { - return PeerMessageHistoryAudioPlaylistItem(entry: view.entries.first!) - } + var tagMask: MessageTags? + switch item.entry { + case let .MessageEntry(message, _, _, _): + for media in message.media { + if let file = media as? TelegramMediaFile { + if file.isVoice { + tagMask = .Voice + } else { + tagMask = .Music } + break } - index += 1 } - if !view.entries.isEmpty { - return PeerMessageHistoryAudioPlaylistItem(entry: view.entries.first!) - } else { - return nil + case .HoleEntry: + break + } + if let tagMask = tagMask { + return account.postbox.aroundMessageHistoryViewForPeerId(item.entry.index.id.peerId, index: item.entry.index, count: 10, anchorIndex: item.entry.index, fixedCombinedReadState: nil, topTaggedMessageIdNamespaces: [], tagMask: tagMask, orderStatistics: []) + |> take(1) + |> map { (view, _, _) -> AudioPlaylistItem? in + var index = 0 + for entry in view.entries { + if entry.index.id == item.entry.index.id { + switch navigation { + case .previous: + if index != 0 { + return PeerMessageHistoryAudioPlaylistItem(entry: view.entries[index - 1]) + } else { + return PeerMessageHistoryAudioPlaylistItem(entry: view.entries.first!) + } + case .next: + if index + 1 < view.entries.count { + return PeerMessageHistoryAudioPlaylistItem(entry: view.entries[index + 1]) + } else { + return nil//PeerMessageHistoryAudioPlaylistItem(entry: view.entries.last!) + } + } + } + index += 1 + } + if !view.entries.isEmpty { + return PeerMessageHistoryAudioPlaylistItem(entry: view.entries.first!) + } else { + return nil + } } - } + } else { + return .single(nil) + } } else { return account.postbox.messageAtId(messageId) |> map { message -> AudioPlaylistItem? in if let message = message { - return PeerMessageHistoryAudioPlaylistItem(entry: .MessageEntry(message, false, nil)) + return PeerMessageHistoryAudioPlaylistItem(entry: .MessageEntry(message, false, nil, nil)) } else { return nil } diff --git a/TelegramUI/PeerMediaCollectionController.swift b/TelegramUI/PeerMediaCollectionController.swift index ed1775d01e..5166bed329 100644 --- a/TelegramUI/PeerMediaCollectionController.swift +++ b/TelegramUI/PeerMediaCollectionController.swift @@ -32,6 +32,8 @@ public class PeerMediaCollectionController: ViewController { private var controllerInteraction: ChatControllerInteraction? private var interfaceInteraction: ChatPanelInterfaceInteraction? + private let messageContextDisposable = MetaDisposable() + public init(account: Account, peerId: PeerId, messageId: MessageId? = nil) { self.account = account self.peerId = peerId @@ -51,7 +53,7 @@ public class PeerMediaCollectionController: ViewController { self.scrollToTop = { [weak self] in if let strongSelf = self, strongSelf.isNodeLoaded { - //strongSelf.chatDisplayNode.historyNode.scrollToStartOfHistory() + strongSelf.mediaCollectionDisplayNode.historyNode.scrollToEndOfHistory() } } @@ -175,9 +177,7 @@ public class PeerMediaCollectionController: ViewController { self?.view.endEditing(true) }, toggleMessageSelection: { [weak self] id in if let strongSelf = self, strongSelf.isNodeLoaded { - if let message = strongSelf.mediaCollectionDisplayNode.historyNode.messageInCurrentHistoryView(id) { - strongSelf.updateInterfaceState(animated: true, { $0.withToggledSelectedMessage(id) }) - } + strongSelf.updateInterfaceState(animated: true, { $0.withToggledSelectedMessage(id) }) } }, sendMessage: { _ in },sendSticker: { _ in @@ -196,8 +196,93 @@ public class PeerMediaCollectionController: ViewController { self.interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { _ in }, setupEditMessage: { _ in }, beginMessageSelection: { _ in - }, deleteSelectedMessages: { - }, forwardSelectedMessages: { + }, deleteSelectedMessages: { [weak self] in + if let strongSelf = self { + if let messageIds = strongSelf.interfaceState.selectionState?.selectedIds, !messageIds.isEmpty { + strongSelf.messageContextDisposable.set((combineLatest(chatDeleteMessagesOptions(account: strongSelf.account, messageIds: messageIds), strongSelf.peer.get() |> take(1)) |> deliverOnMainQueue).start(next: { options, peer in + if let strongSelf = self, let peer = peer, !options.isEmpty { + let actionSheet = ActionSheetController() + var items: [ActionSheetItem] = [] + var personalPeerName: String? + var isChannel = false + if let user = peer as? TelegramUser { + personalPeerName = user.compactDisplayTitle + } else if let channel = 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.updateInterfaceState(animated: true, { $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.updateInterfaceState(animated: true, { $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) + } + })) + } + } + }, forwardSelectedMessages: { [weak self] in + if let strongSelf = self { + if let forwardMessageIdsSet = strongSelf.interfaceState.selectionState?.selectedIds { + let forwardMessageIds = Array(forwardMessageIdsSet).sorted() + + let controller = PeerSelectionController(account: strongSelf.account) + controller.peerSelected = { [weak controller] peerId in + if let strongSelf = self, let _ = controller { + let _ = (strongSelf.account.postbox.modify({ modifier -> Void in + modifier.updatePeerChatInterfaceState(peerId, update: { currentState in + if let currentState = currentState as? ChatInterfaceState { + return currentState.withUpdatedForwardMessageIds(forwardMessageIds) + } else { + return ChatInterfaceState().withUpdatedForwardMessageIds(forwardMessageIds) + } + }) + }) |> deliverOnMainQueue).start(completed: { + if let strongSelf = self { + strongSelf.updateInterfaceState(animated: false, { $0.withoutSelectionState() }) + + let ready = ValuePromise() + + strongSelf.messageContextDisposable.set((ready.get() |> take(1) |> deliverOnMainQueue).start(next: { _ in + if let strongController = controller { + strongController.dismiss() + } + })) + + (strongSelf.navigationController as? NavigationController)?.replaceTopController(ChatController(account: strongSelf.account, peerId: peerId), animated: false, ready: ready) + } + }) + } + } + strongSelf.present(controller, in: .window) + } + } }, updateTextInputState: { _ in }, updateInputModeAndDismissedButtonKeyboardMessageId: { _ in }, editMessage: { _, _ in @@ -245,6 +330,7 @@ public class PeerMediaCollectionController: ViewController { self.messageIndexDisposable.dispose() self.navigationActionDisposable.dispose() self.galleryHiddenMesageAndMediaDisposable.dispose() + self.messageContextDisposable.dispose() } var mediaCollectionDisplayNode: PeerMediaCollectionControllerNode { @@ -254,7 +340,7 @@ public class PeerMediaCollectionController: ViewController { } override public func loadDisplayNode() { - self.displayNode = PeerMediaCollectionControllerNode(account: self.account, peerId: self.peerId, messageId: self.messageId, controllerInteraction: self.controllerInteraction!) + self.displayNode = PeerMediaCollectionControllerNode(account: self.account, peerId: self.peerId, messageId: self.messageId, controllerInteraction: self.controllerInteraction!, interfaceInteraction: self.interfaceInteraction!) self.ready.set(combineLatest(self.mediaCollectionDisplayNode.historyNode.historyState.get(), self._peerReady.get()) |> map { $1 }) diff --git a/TelegramUI/PeerMediaCollectionControllerNode.swift b/TelegramUI/PeerMediaCollectionControllerNode.swift index 5849e05695..02523a148b 100644 --- a/TelegramUI/PeerMediaCollectionControllerNode.swift +++ b/TelegramUI/PeerMediaCollectionControllerNode.swift @@ -28,6 +28,7 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { private let account: Account private let peerId: PeerId private let controllerInteraction: ChatControllerInteraction + private let interfaceInteraction: ChatPanelInterfaceInteraction private var historyNodeImpl: ASDisplayNode var historyNode: ChatHistoryNode { @@ -47,10 +48,11 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { private var modeSelectionNode: PeerMediaCollectionModeSelectionNode? private var selectionPanel: ChatMessageSelectionInputPanelNode? - init(account: Account, peerId: PeerId, messageId: MessageId?, controllerInteraction: ChatControllerInteraction) { + init(account: Account, peerId: PeerId, messageId: MessageId?, controllerInteraction: ChatControllerInteraction, interfaceInteraction: ChatPanelInterfaceInteraction) { self.account = account self.peerId = peerId self.controllerInteraction = controllerInteraction + self.interfaceInteraction = interfaceInteraction self.historyNodeImpl = historyNodeImplForMode(self.mediaCollectionInterfaceState.mode, account: account, peerId: peerId, messageId: messageId, controllerInteraction: controllerInteraction) @@ -82,6 +84,7 @@ class PeerMediaCollectionControllerNode: ASDisplayNode { transition.updateFrame(node: selectionPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - panelHeight), size: CGSize(width: layout.size.width, height: panelHeight))) } else { let selectionPanel = ChatMessageSelectionInputPanelNode() + selectionPanel.interfaceInteraction = self.interfaceInteraction selectionPanel.selectedMessageCount = selectionState.selectedIds.count selectionPanel.backgroundColor = UIColor(0xfafafa) let panelHeight = selectionPanel.updateLayout(width: layout.size.width, transition: .immediate, interfaceState: interfaceState) diff --git a/TelegramUI/PhotoResources.swift b/TelegramUI/PhotoResources.swift index b815ca76af..7c2162121d 100644 --- a/TelegramUI/PhotoResources.swift +++ b/TelegramUI/PhotoResources.swift @@ -118,6 +118,53 @@ private func chatMessageFileDatas(account: Account, file: TelegramMediaFile, pro } } +private func chatMessageVideoDatas(account: Account, file: TelegramMediaFile) -> Signal<(Data?, (Data, String)?, Bool), NoError> { + if let smallestRepresentation = smallestImageRepresentation(file.previewRepresentations) { + let thumbnailResource = smallestRepresentation.resource + let fullSizeResource = file.resource + + let maybeFullSize = account.postbox.mediaBox.cachedResourceRepresentation(fullSizeResource, representation: CachedVideoFirstFrameRepresentation(), complete: false) + + let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, (Data, String)?, Bool), NoError> in + if maybeData.complete { + let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) + + return .single((nil, loadedData == nil ? nil : (loadedData!, maybeData.path), true)) + } else { + let fetchedThumbnail = account.postbox.mediaBox.fetchedResource(thumbnailResource) + + let thumbnail = Signal { subscriber in + let fetchedDisposable = fetchedThumbnail.start() + let thumbnailDisposable = account.postbox.mediaBox.resourceData(thumbnailResource).start(next: { next in + subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) + }, error: subscriber.putError, completed: subscriber.putCompletion) + + return ActionDisposable { + fetchedDisposable.dispose() + thumbnailDisposable.dispose() + } + } + + + let fullSizeDataAndPath = maybeFullSize |> map { next -> ((Data, String)?, Bool) in + let data = next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe) + return (data == nil ? nil : (data!, next.path), next.complete) + } + + return thumbnail |> mapToSignal { thumbnailData in + return fullSizeDataAndPath |> map { (dataAndPath, complete) in + return (thumbnailData, dataAndPath, complete) + } + } + } + } |> filter({ $0.0 != nil || $0.1 != nil }) + + return signal + } else { + return .never() + } +} + private enum Corner: Hashable { case TopLeft(Int), TopRight(Int), BottomLeft(Int), BottomRight(Int) @@ -475,7 +522,7 @@ private func chatMessagePhotoThumbnailDatas(account: Account, photo: TelegramMed let fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0) if let smallestRepresentation = smallestImageRepresentation(photo.representations), let largestRepresentation = photo.representationForDisplayAtSize(fullRepresentationSize) { - let maybeFullSize = account.postbox.mediaBox.cachedResourceRepresentation(largestRepresentation.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 160.0, height: 160.0))) + let maybeFullSize = account.postbox.mediaBox.cachedResourceRepresentation(largestRepresentation.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 160.0, height: 160.0)), complete: false) let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, Data?, Bool), NoError> in if maybeData.complete { @@ -795,7 +842,83 @@ func mediaGridMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signa } } - +func mediaGridMessageVideo(account: Account, video: TelegramMediaFile) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let signal = chatMessageVideoDatas(account: account, file: video) + + return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in + return { arguments in + assertNotOnMainThread() + let context = DrawingContext(size: arguments.drawingSize, clear: true) + + let drawingRect = arguments.drawingRect + let fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) + let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + + var fullSizeImage: CGImage? + if let fullSizeData = fullSizeData { + if fullSizeComplete { + let options = NSMutableDictionary() + options.setValue(max(fittedSize.width * context.scale, fittedSize.height * context.scale) as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) + options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) + if let imageSource = CGImageSourceCreateWithData(fullSizeData.0 as CFData, nil), let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) { + fullSizeImage = image + } + } else { + let imageSource = CGImageSourceCreateIncremental(nil) + CGImageSourceUpdateData(imageSource, fullSizeData.0 as CFData, fullSizeComplete) + + let options = NSMutableDictionary() + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + if let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { + fullSizeImage = image + } + } + } + + var thumbnailImage: CGImage? + if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { + thumbnailImage = image + } + + var blurredThumbnailImage: UIImage? + if let thumbnailImage = thumbnailImage { + let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) + let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 150.0, height: 150.0)) + let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) + thumbnailContext.withFlippedContext { c in + c.interpolationQuality = .none + c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) + } + telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + + blurredThumbnailImage = thumbnailContext.generateImage() + } + + context.withFlippedContext { c in + c.setBlendMode(.copy) + if arguments.boundingSize != arguments.imageSize { + c.fill(arguments.drawingRect) + } + + c.setBlendMode(.copy) + if let blurredThumbnailImage = blurredThumbnailImage, let cgImage = blurredThumbnailImage.cgImage { + c.interpolationQuality = .low + c.draw(cgImage, in: fittedRect) + c.setBlendMode(.normal) + } + + if let fullSizeImage = fullSizeImage { + c.interpolationQuality = .medium + c.draw(fullSizeImage, in: fittedRect) + } + } + + addCorners(context, arguments: arguments) + + return context + } + } +} func chatMessagePhotoStatus(account: Account, photo: TelegramMediaImage) -> Signal { if let largestRepresentation = largestRepresentationForPhoto(photo) { diff --git a/TelegramUI/PresenceStrings.swift b/TelegramUI/PresenceStrings.swift index b03116e62e..aede1fbef1 100644 --- a/TelegramUI/PresenceStrings.swift +++ b/TelegramUI/PresenceStrings.swift @@ -31,6 +31,41 @@ func shortStringForDayOfWeek(_ day: Int32) -> String { } } +func stringForMonth(_ month: Int32) -> String { + switch month { + case 0: + return "January" + case 1: + return "February" + case 2: + return "March" + case 3: + return "April" + case 4: + return "May" + case 5: + return "June" + case 6: + return "July" + case 7: + return "August" + case 8: + return "September" + case 9: + return "October" + case 10: + return "November" + case 11: + return "December" + default: + return "" + } +} + +func stringForMonth(_ month: Int32, ofYear year: Int32) -> String { + return stringForMonth(month) + " \(1900 + year)" +} + func stringForTime(hours: Int32, minutes: Int32) -> String { return String(format: "%d:%02d", hours, minutes) } diff --git a/TelegramUI/SearchBarPlaceholderNode.swift b/TelegramUI/SearchBarPlaceholderNode.swift index d021b26cc0..df485b44fb 100644 --- a/TelegramUI/SearchBarPlaceholderNode.swift +++ b/TelegramUI/SearchBarPlaceholderNode.swift @@ -69,7 +69,7 @@ class SearchBarPlaceholderNode: ASDisplayNode, ASEditableTextNodeDelegate { let currentForegroundColor = self.foregroundColor return { placeholderString, constrainedSize, foregroundColor in - let (labelLayoutResult, labelApply) = labelLayout(placeholderString, foregroundColor, 1, .end, constrainedSize, .natural, nil) + let (labelLayoutResult, labelApply) = labelLayout(placeholderString, foregroundColor, 1, .end, constrainedSize, .natural, nil, UIEdgeInsets()) var updatedBackgroundImage: UIImage? if !currentForegroundColor.isEqual(foregroundColor) { diff --git a/TelegramUI/SearchDisplayController.swift b/TelegramUI/SearchDisplayController.swift index f58a306187..ce075a8029 100644 --- a/TelegramUI/SearchDisplayController.swift +++ b/TelegramUI/SearchDisplayController.swift @@ -74,4 +74,8 @@ final class SearchDisplayController { contentNode.removeFromSupernode() } } + + func previewViewAndActionAtLocation(_ location: CGPoint) -> (UIView, Any)? { + return self.contentNode.previewViewAndActionAtLocation(location) + } } diff --git a/TelegramUI/SearchDisplayControllerContentNode.swift b/TelegramUI/SearchDisplayControllerContentNode.swift index 9ab0ba6833..8bf4827485 100644 --- a/TelegramUI/SearchDisplayControllerContentNode.swift +++ b/TelegramUI/SearchDisplayControllerContentNode.swift @@ -21,4 +21,8 @@ class SearchDisplayControllerContentNode: ASDisplayNode { func ready() -> Signal { return .single(Void()) } + + func previewViewAndActionAtLocation(_ location: CGPoint) -> (UIView, Any)? { + return nil + } } diff --git a/TelegramUI/SecretMediaPreviewController.swift b/TelegramUI/SecretMediaPreviewController.swift index cd4fcc8cc8..edacfe9a5d 100644 --- a/TelegramUI/SecretMediaPreviewController.swift +++ b/TelegramUI/SecretMediaPreviewController.swift @@ -68,7 +68,7 @@ public final class SecretMediaPreviewController: ViewController { if let messageView = self.messageView, let message = messageView.message { if self.currentNodeMessageId != message.id { self.currentNodeMessageId = message.id - let item = galleryItemForEntry(account: account, entry: .MessageEntry(message, false, nil)) + let item = galleryItemForEntry(account: account, entry: .MessageEntry(message, false, nil, nil)) let itemNode = item.node() self.controllerNode.setItemNode(itemNode) @@ -77,7 +77,7 @@ public final class SecretMediaPreviewController: ViewController { } self._ready.set(ready |> map { true }) - self.markMessageAsConsumedDisposable.set(markMessageContentAsConsumedInteractively(postbox: self.account.postbox, network: self.account.network, messageId: message.id).start()) + self.markMessageAsConsumedDisposable.set(markMessageContentAsConsumedInteractively(postbox: self.account.postbox, messageId: message.id).start()) } } else { if !self.didSetReady { diff --git a/TelegramUI/SettingsAccountInfoItem.swift b/TelegramUI/SettingsAccountInfoItem.swift index 1d5a90ee5f..8c10a6c8df 100644 --- a/TelegramUI/SettingsAccountInfoItem.swift +++ b/TelegramUI/SettingsAccountInfoItem.swift @@ -62,7 +62,7 @@ class SettingsAccountInfoItemNode: ListControllerGroupableItemNode { return { item, width in if let item = item as? SettingsAccountInfoItem { - let (nameNodeLayout, nameNodeApply) = layoutNameNode(NSAttributedString(string: item.peer?.displayTitle ?? "", font: nameFont, textColor: UIColor.black), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (nameNodeLayout, nameNodeApply) = layoutNameNode(NSAttributedString(string: item.peer?.displayTitle ?? "", font: nameFont, textColor: UIColor.black), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) let statusText: String let statusColor: UIColor @@ -81,7 +81,7 @@ class SettingsAccountInfoItemNode: ListControllerGroupableItemNode { statusColor = UIColor(0x007ee5) } - let (statusNodeLayout, statusNodeApply) = layoutStatusNode(NSAttributedString(string: statusText, font: statusFont, textColor: statusColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil) + let (statusNodeLayout, statusNodeApply) = layoutStatusNode(NSAttributedString(string: statusText, font: statusFont, textColor: statusColor), nil, 1, .end, CGSize(width: width - 20, height: CGFloat.greatestFiniteMagnitude), .natural, nil, UIEdgeInsets()) return (CGSize(width: width, height: 97.0), { [weak self] in if let strongSelf = self { diff --git a/TelegramUI/StickerResources.swift b/TelegramUI/StickerResources.swift index 5ce42a213b..eabc977f9a 100644 --- a/TelegramUI/StickerResources.swift +++ b/TelegramUI/StickerResources.swift @@ -36,7 +36,7 @@ private func imageFromAJpeg(data: Data) -> (UIImage, UIImage)? { private func chatMessageStickerDatas(account: Account, file: TelegramMediaFile, small: Bool) -> Signal<(Data?, Data?, Bool), NoError> { //let maybeFetched = account.postbox.mediaBox.resourceData(file.resource, complete: true) - let maybeFetched = account.postbox.mediaBox.cachedResourceRepresentation(file.resource, representation: CachedStickerAJpegRepresentation(size: small ? CGSize(width: 160.0, height: 160.0) : nil)) + let maybeFetched = account.postbox.mediaBox.cachedResourceRepresentation(file.resource, representation: CachedStickerAJpegRepresentation(size: small ? CGSize(width: 160.0, height: 160.0) : nil), complete: false) return maybeFetched |> take(1) |> mapToSignal { maybeData in if maybeData.complete { @@ -46,7 +46,7 @@ private func chatMessageStickerDatas(account: Account, file: TelegramMediaFile, } else { //let fullSizeData = account.postbox.mediaBox.resourceData(file.resource, complete: true) - let fullSizeData = account.postbox.mediaBox.cachedResourceRepresentation(file.resource, representation: CachedStickerAJpegRepresentation(size: small ? CGSize(width: 160.0, height: 160.0) : nil)) |> map { next in + let fullSizeData = account.postbox.mediaBox.cachedResourceRepresentation(file.resource, representation: CachedStickerAJpegRepresentation(size: small ? CGSize(width: 160.0, height: 160.0) : nil), complete: false) |> map { next in return (next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe), next.complete) } diff --git a/TelegramUI/TelegramAccountAuxiliaryMethods.swift b/TelegramUI/TelegramAccountAuxiliaryMethods.swift index b413d84128..02e8a60ed7 100644 --- a/TelegramUI/TelegramAccountAuxiliaryMethods.swift +++ b/TelegramUI/TelegramAccountAuxiliaryMethods.swift @@ -15,6 +15,8 @@ public let telegramAccountAuxiliaryMethods = AccountAuxiliaryMethods(updatePeerC return fetchVideoLibraryMediaResource(resource: resource) } else if let resource = resource as? LocalFileVideoMediaResource { return fetchLocalFileVideoMediaResource(resource: resource) + } else if let photoLibraryResource = resource as? PhotoLibraryMediaResource { + return fetchPhotoLibraryResource(localIdentifier: photoLibraryResource.localIdentifier) } return nil }) diff --git a/TelegramUI/TelegramController.swift b/TelegramUI/TelegramController.swift index dc5c263351..a8133dee60 100644 --- a/TelegramUI/TelegramController.swift +++ b/TelegramUI/TelegramController.swift @@ -13,7 +13,7 @@ public class TelegramController: ViewController { override public var navigationHeight: CGFloat { var height = super.navigationHeight - if let mediaAccessoryPanel = self.mediaAccessoryPanel { + if let _ = self.mediaAccessoryPanel { height += 36.0 } return height diff --git a/TelegramUI/TextNode.swift b/TelegramUI/TextNode.swift index 5b578ba607..cfcee8cf24 100644 --- a/TelegramUI/TextNode.swift +++ b/TelegramUI/TextNode.swift @@ -38,16 +38,18 @@ final class TextNodeLayout: NSObject { fileprivate let constrainedSize: CGSize fileprivate let alignment: NSTextAlignment fileprivate let cutout: TextNodeCutout? + fileprivate let insets: UIEdgeInsets let size: CGSize fileprivate let lines: [TextNodeLine] - fileprivate init(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment, cutout: TextNodeCutout?, size: CGSize, lines: [TextNodeLine], backgroundColor: UIColor?) { + fileprivate init(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment, cutout: TextNodeCutout?, insets: UIEdgeInsets, size: CGSize, lines: [TextNodeLine], backgroundColor: UIColor?) { self.attributedString = attributedString self.maximumNumberOfLines = maximumNumberOfLines self.truncationType = truncationType self.constrainedSize = constrainedSize self.alignment = alignment self.cutout = cutout + self.insets = insets self.size = size self.lines = lines self.backgroundColor = backgroundColor @@ -127,7 +129,7 @@ final class TextNode: ASDisplayNode { } } - private class func calculateLayout(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, cutout: TextNodeCutout?) -> TextNodeLayout { + private class func calculateLayout(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, cutout: TextNodeCutout?, insets: UIEdgeInsets) -> TextNodeLayout { if let attributedString = attributedString { let stringLength = attributedString.length @@ -152,7 +154,7 @@ final class TextNode: ASDisplayNode { var maybeTypesetter: CTTypesetter? maybeTypesetter = CTTypesetterCreateWithAttributedString(attributedString as CFAttributedString) if maybeTypesetter == nil { - return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, cutout: cutout, size: CGSize(), lines: [], backgroundColor: backgroundColor) + return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, cutout: cutout, insets: insets, size: CGSize(), lines: [], backgroundColor: backgroundColor) } let typesetter = maybeTypesetter! @@ -255,9 +257,9 @@ final class TextNode: ASDisplayNode { } } - return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, cutout: cutout, size: CGSize(width: ceil(layoutSize.width), height: ceil(layoutSize.height)), lines: lines, backgroundColor: backgroundColor) + return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, cutout: cutout, insets: insets, size: CGSize(width: ceil(layoutSize.width) + insets.left + insets.right, height: ceil(layoutSize.height) + insets.top + insets.bottom), lines: lines, backgroundColor: backgroundColor) } else { - return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, cutout: cutout, size: CGSize(), lines: [], backgroundColor: backgroundColor) + return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, cutout: cutout, insets: insets, size: CGSize(), lines: [], backgroundColor: backgroundColor) } } @@ -295,6 +297,7 @@ final class TextNode: ASDisplayNode { //let clipRect = CGContextGetClipBoundingBox(context) let alignment = layout.alignment + let offset = CGPoint(x: layout.insets.left, y: layout.insets.top) for i in 0 ..< layout.lines.count { let line = layout.lines[i] @@ -304,7 +307,7 @@ final class TextNode: ASDisplayNode { } else { lineOffset = 0.0 } - context.textPosition = CGPoint(x: line.frame.origin.x + lineOffset, y: line.frame.origin.y) + context.textPosition = CGPoint(x: line.frame.origin.x + lineOffset + offset.x, y: line.frame.origin.y + offset.y) CTLineDraw(line.line, context) } @@ -316,10 +319,10 @@ final class TextNode: ASDisplayNode { context.setBlendMode(.normal) } - class func asyncLayout(_ maybeNode: TextNode?) -> (_ attributedString: NSAttributedString?, _ backgroundColor: UIColor?, _ maximumNumberOfLines: Int, _ truncationType: CTLineTruncationType, _ constrainedSize: CGSize, _ alignment: NSTextAlignment, _ cutout: TextNodeCutout?) -> (TextNodeLayout, () -> TextNode) { + class func asyncLayout(_ maybeNode: TextNode?) -> (_ attributedString: NSAttributedString?, _ backgroundColor: UIColor?, _ maximumNumberOfLines: Int, _ truncationType: CTLineTruncationType, _ constrainedSize: CGSize, _ alignment: NSTextAlignment, _ cutout: TextNodeCutout?, _ insets: UIEdgeInsets) -> (TextNodeLayout, () -> TextNode) { let existingLayout: TextNodeLayout? = maybeNode?.cachedLayout - return { attributedString, backgroundColor, maximumNumberOfLines, truncationType, constrainedSize, alignment, cutout in + return { attributedString, backgroundColor, maximumNumberOfLines, truncationType, constrainedSize, alignment, cutout, insets in let layout: TextNodeLayout var updated = false @@ -348,11 +351,11 @@ final class TextNode: ASDisplayNode { if stringMatch { layout = existingLayout } else { - layout = TextNode.calculateLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, backgroundColor: backgroundColor, constrainedSize: constrainedSize, alignment: alignment, cutout: cutout) + layout = TextNode.calculateLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, backgroundColor: backgroundColor, constrainedSize: constrainedSize, alignment: alignment, cutout: cutout, insets: insets) updated = true } } else { - layout = TextNode.calculateLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, backgroundColor: backgroundColor, constrainedSize: constrainedSize, alignment: alignment, cutout: cutout) + layout = TextNode.calculateLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, backgroundColor: backgroundColor, constrainedSize: constrainedSize, alignment: alignment, cutout: cutout, insets: insets) updated = true } diff --git a/TelegramUI/TransformOutgoingMessageMedia.swift b/TelegramUI/TransformOutgoingMessageMedia.swift index 061beb5f22..b6317db0cc 100644 --- a/TelegramUI/TransformOutgoingMessageMedia.swift +++ b/TelegramUI/TransformOutgoingMessageMedia.swift @@ -61,6 +61,43 @@ public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, me return .complete() } } + case let image as TelegramMediaImage: + if let representation = largestImageRepresentation(image.representations) { + let signal = Signal { subscriber in + let fetch = postbox.mediaBox.fetchedResource(representation.resource).start() + let data = postbox.mediaBox.resourceData(representation.resource, option: .complete(waitUntilFetchStatus: true)).start(next: { next in + subscriber.putNext(next) + if next.complete { + subscriber.putCompletion() + } + }) + + return ActionDisposable { + fetch.dispose() + data.dispose() + } + } + + let result: Signal + if opportunistic { + result = signal |> take(1) + } else { + result = signal + } + + return result + |> mapToSignal { data -> Signal in + if data.complete { + return .single(nil) + } else if opportunistic { + return .single(nil) + } else { + return .complete() + } + } + } else { + return .single(nil) + } default: return .single(nil) } diff --git a/TelegramUI/UserInfoController.swift b/TelegramUI/UserInfoController.swift index b1285f3318..33ceaaf03b 100644 --- a/TelegramUI/UserInfoController.swift +++ b/TelegramUI/UserInfoController.swift @@ -13,8 +13,9 @@ private final class UserInfoControllerArguments { let openGroupsInCommon: () -> Void let updatePeerBlocked: (Bool) -> Void let deleteContact: () -> Void + let displayUsernameContextMenu: (String) -> Void - init(account: Account, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, openChat: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openGroupsInCommon: @escaping () -> Void, updatePeerBlocked: @escaping (Bool) -> Void, deleteContact: @escaping () -> Void) { + init(account: Account, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, openChat: @escaping () -> Void, changeNotificationMuteSettings: @escaping () -> Void, openSharedMedia: @escaping () -> Void, openGroupsInCommon: @escaping () -> Void, updatePeerBlocked: @escaping (Bool) -> Void, deleteContact: @escaping () -> Void, displayUsernameContextMenu: @escaping (String) -> Void) { self.account = account self.updateEditingName = updateEditingName self.openChat = openChat @@ -23,6 +24,7 @@ private final class UserInfoControllerArguments { self.openGroupsInCommon = openGroupsInCommon self.updatePeerBlocked = updatePeerBlocked self.deleteContact = deleteContact + self.displayUsernameContextMenu = displayUsernameContextMenu } } @@ -33,6 +35,10 @@ private enum UserInfoSection: ItemListSectionId { case block } +private enum UserInfoEntryTag { + case username +} + private enum UserInfoEntry: ItemListNodeEntry { case info(peer: Peer?, presence: PeerPresence?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState) case about(text: String) @@ -235,11 +241,15 @@ private enum UserInfoEntry: ItemListNodeEntry { arguments.updateEditingName(editingName) }) case let .about(text): - return ItemListTextWithLabelItem(label: "about", text: text, multiline: true, sectionId: self.section) + return ItemListTextWithLabelItem(label: "about", text: text, multiline: true, sectionId: self.section, action: nil) case let .phoneNumber(_, value): - return ItemListTextWithLabelItem(label: value.label, text: formatPhoneNumber(value.number), multiline: false, sectionId: self.section) + return ItemListTextWithLabelItem(label: value.label, text: formatPhoneNumber(value.number), multiline: false, sectionId: self.section, action: { + + }) case let .userName(value): - return ItemListTextWithLabelItem(label: "username", text: "@\(value)", multiline: false, sectionId: self.section) + return ItemListTextWithLabelItem(label: "username", text: "@\(value)", multiline: false, sectionId: self.section, action: { + arguments.displayUsernameContextMenu("@" + value) + }, tag: UserInfoEntryTag.username) case .sendMessage: return ItemListActionItem(title: "Send Message", kind: .generic, alignment: .natural, sectionId: self.section, style: .plain, action: { arguments.openChat() @@ -432,6 +442,7 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll var pushControllerImpl: ((ViewController) -> Void)? var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments) -> Void)? var openChatImpl: (() -> Void)? + var displayUsernameContextMenuImpl: ((String) -> Void)? let actionsDisposable = DisposableSet() @@ -510,6 +521,8 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll updatePeerBlockedDisposable.set(requestUpdatePeerIsBlocked(account: account, peerId: peerId, isBlocked: value).start()) }, deleteContact: { + }, displayUsernameContextMenu: { text in + displayUsernameContextMenuImpl?(text) }) let signal = combineLatest(statePromise.get(), account.viewTracker.peerView(peerId), account.postbox.combinedView(keys: [.peerChatState(peerId: peerId)])) @@ -593,5 +606,34 @@ public func userInfoController(account: Account, peerId: PeerId) -> ViewControll navigateToChatController(navigationController: navigationController, account: account, peerId: peerId) } } + displayUsernameContextMenuImpl = { [weak controller] text in + if let strongController = controller { + var resultItemNode: ListViewItemNode? + let _ = strongController.frameForItemNode({ itemNode in + if let itemNode = itemNode as? ItemListTextWithLabelItemNode { + if let tag = itemNode.tag as? UserInfoEntryTag { + if tag == .username { + resultItemNode = itemNode + return true + } + } + } + return false + }) + if let resultItemNode = resultItemNode { + let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text("Copy"), action: { + UIPasteboard.general.string = text + })]) + strongController.present(contextMenuController, in: .window, with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in + if let resultItemNode = resultItemNode { + return (resultItemNode, resultItemNode.contentBounds.insetBy(dx: 0.0, dy: -2.0)) + } else { + return nil + } + })) + + } + } + } return controller } diff --git a/TelegramUI/VerticalListContextResultsChatInputPanelButtonItem.swift b/TelegramUI/VerticalListContextResultsChatInputPanelButtonItem.swift index 2c390bde1d..f760c78bfc 100644 --- a/TelegramUI/VerticalListContextResultsChatInputPanelButtonItem.swift +++ b/TelegramUI/VerticalListContextResultsChatInputPanelButtonItem.swift @@ -126,7 +126,7 @@ final class VerticalListContextResultsChatInputPanelButtonItemNode: ListViewItem return { [weak self] item, width, mergedTop, mergedBottom in let titleString = NSAttributedString(string: item.title, font: titleFont, textColor: UIColor(0x007ee5)) - let (titleLayout, titleApply) = makeTitleLayout(titleString, nil, 1, .end, CGSize(width: width - 16.0, height: 100.0), .natural, nil) + let (titleLayout, titleApply) = makeTitleLayout(titleString, nil, 1, .end, CGSize(width: width - 16.0, height: 100.0), .natural, nil, UIEdgeInsets()) let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: VerticalListContextResultsChatInputPanelButtonItemNode.itemHeight), insets: UIEdgeInsets()) diff --git a/TelegramUI/VerticalListContextResultsChatInputPanelItem.swift b/TelegramUI/VerticalListContextResultsChatInputPanelItem.swift index 45f74a2397..05d19c6945 100644 --- a/TelegramUI/VerticalListContextResultsChatInputPanelItem.swift +++ b/TelegramUI/VerticalListContextResultsChatInputPanelItem.swift @@ -225,11 +225,11 @@ final class VerticalListContextResultsChatInputPanelItemNode: ListViewItemNode { } } - let (titleLayout, titleApply) = makeTitleLayout(titleString, nil, 1, .end, CGSize(width: width - leftInset - rightInset, height: 100.0), .natural, nil) + let (titleLayout, titleApply) = makeTitleLayout(titleString, nil, 1, .end, CGSize(width: width - leftInset - rightInset, height: 100.0), .natural, nil, UIEdgeInsets()) - let (textLayout, textApply) = makeTextLayout(textString, nil, 2, .end, CGSize(width: width - leftInset - rightInset, height: 100.0), .natural, nil) + let (textLayout, textApply) = makeTextLayout(textString, nil, 2, .end, CGSize(width: width - leftInset - rightInset, height: 100.0), .natural, nil, UIEdgeInsets()) - let (iconTextLayout, iconTextApply) = iconTextMakeLayout(iconText, nil, 1, .end, CGSize(width: 38.0, height: CGFloat.infinity), .natural, nil) + let (iconTextLayout, iconTextApply) = iconTextMakeLayout(iconText, nil, 1, .end, CGSize(width: 38.0, height: CGFloat.infinity), .natural, nil, UIEdgeInsets()) var titleFrame: CGRect? if let _ = titleString { diff --git a/TelegramUI/VideoPlayerProxy.swift b/TelegramUI/VideoPlayerProxy.swift new file mode 100644 index 0000000000..fbf287572c --- /dev/null +++ b/TelegramUI/VideoPlayerProxy.swift @@ -0,0 +1,2 @@ +import Foundation +