Added galleries for web search results

This commit is contained in:
Ilya Laktyushin 2018-12-14 22:40:34 +04:00
parent feaaed3f41
commit 98de1f903f
46 changed files with 2874 additions and 1937 deletions

View File

@ -90,6 +90,8 @@
09DD88F321BF907C000766BC /* WebSearchRecentQueryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09DD88F221BF907C000766BC /* WebSearchRecentQueryItem.swift */; };
09DD88F521BF9730000766BC /* WebSearchRecentQueries.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09DD88F421BF9730000766BC /* WebSearchRecentQueries.swift */; };
09DD88FA21BFD70B000766BC /* ThemedTextAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09DD88F921BFD70B000766BC /* ThemedTextAlertController.swift */; };
09F799FA21C3542D00820234 /* LegacyWebSearchGallery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09F799F921C3542D00820234 /* LegacyWebSearchGallery.swift */; };
09F799FC21C3FF3000820234 /* WebSearchGalleryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09F799FB21C3FF3000820234 /* WebSearchGalleryController.swift */; };
09FE756D2153F5F900A3120F /* CallRouteActionSheetItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09FE756C2153F5F900A3120F /* CallRouteActionSheetItem.swift */; };
9F06830921A404AB001D8EDB /* NotificationExceptionControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F06830821A404AB001D8EDB /* NotificationExceptionControllerNode.swift */; };
9F06830B21A404C4001D8EDB /* NotificationExcetionSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F06830A21A404C4001D8EDB /* NotificationExcetionSettingsController.swift */; };
@ -1178,6 +1180,8 @@
09DD88F221BF907C000766BC /* WebSearchRecentQueryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSearchRecentQueryItem.swift; sourceTree = "<group>"; };
09DD88F421BF9730000766BC /* WebSearchRecentQueries.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSearchRecentQueries.swift; sourceTree = "<group>"; };
09DD88F921BFD70B000766BC /* ThemedTextAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemedTextAlertController.swift; sourceTree = "<group>"; };
09F799F921C3542D00820234 /* LegacyWebSearchGallery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyWebSearchGallery.swift; sourceTree = "<group>"; };
09F799FB21C3FF3000820234 /* WebSearchGalleryController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSearchGalleryController.swift; sourceTree = "<group>"; };
09FE756C2153F5F900A3120F /* CallRouteActionSheetItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallRouteActionSheetItem.swift; sourceTree = "<group>"; };
9F06830821A404AB001D8EDB /* NotificationExceptionControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationExceptionControllerNode.swift; sourceTree = "<group>"; };
9F06830A21A404C4001D8EDB /* NotificationExcetionSettingsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationExcetionSettingsController.swift; sourceTree = "<group>"; };
@ -2360,6 +2364,7 @@
0962E67E21BA786A00245FD9 /* WebSearchItem.swift */,
09DD88F221BF907C000766BC /* WebSearchRecentQueryItem.swift */,
09DD88F421BF9730000766BC /* WebSearchRecentQueries.swift */,
09F799FB21C3FF3000820234 /* WebSearchGalleryController.swift */,
);
name = "Web Search";
sourceTree = "<group>";
@ -3171,6 +3176,7 @@
D07ABBAA202A1BD1003671DE /* LegacyWallpaperEditor.swift */,
D0380DAC204ED434000414AB /* LegacyLiveUploadInterface.swift */,
D097C26B20DD1EA5007BB4B8 /* OverlayStatusController.swift */,
09F799F921C3542D00820234 /* LegacyWebSearchGallery.swift */,
);
name = "Legacy Components";
sourceTree = "<group>";
@ -5035,6 +5041,7 @@
D01776B51F1D6CCC0044446D /* RadialStatusContentNode.swift in Sources */,
D02F4AF01FD4C46D004DFBAE /* SystemVideoContent.swift in Sources */,
D0477D1F1F619E0700412B44 /* GalleryVideoDecoration.swift in Sources */,
09F799FA21C3542D00820234 /* LegacyWebSearchGallery.swift in Sources */,
D01C99781F4F382C00DCFAF6 /* InstantPageSettingsItemTheme.swift in Sources */,
D0CAD8FB20AE1D1B00ACD96E /* ChannelMemberCategoryListContext.swift in Sources */,
D0EC6CC21EB9F58800EBF1C3 /* LegacyEmptyController.swift in Sources */,
@ -5585,6 +5592,7 @@
D0EC6DD41EB9F58900EBF1C3 /* CommandChatInputContextPanelNode.swift in Sources */,
D0EC6DD51EB9F58900EBF1C3 /* CommandChatInputPanelItem.swift in Sources */,
D0EC6DD61EB9F58900EBF1C3 /* VerticalListContextResultsChatInputContextPanelNode.swift in Sources */,
09F799FC21C3FF3000820234 /* WebSearchGalleryController.swift in Sources */,
D0EC6DD71EB9F58900EBF1C3 /* VerticalListContextResultsChatInputPanelItem.swift in Sources */,
D0EC6DD81EB9F58900EBF1C3 /* VerticalListContextResultsChatInputPanelButtonItem.swift in Sources */,
D04281F4200E5AB0009DDE36 /* ChatRecentActionsController.swift in Sources */,

View File

@ -2,6 +2,7 @@ import Foundation
import SwiftSignalKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
private final class CallRatingContentActionNode: HighlightableButtonNode {
@ -320,8 +321,19 @@ private final class CallRatingAlertContentNode: AlertContentNode {
}
}
private func rateCallAndSendLogs(account: Account, report: ReportCallRating, starsCount: Int, comment: String, includeLogs: Bool) {
let _ = rateCall(account: account, report: report, starsCount: Int32(starsCount), comment: comment).start()
private func rateCallAndSendLogs(account: Account, report: ReportCallRating, starsCount: Int, comment: String, includeLogs: Bool) -> Signal<Void, NoError> {
var signal = rateCall(account: account, report: report, starsCount: Int32(starsCount), comment: comment)
if includeLogs {
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: 4244000)
// signal = signal
// |> then(
// enqueueMessages(account: account, peerId: peerId, messages: EnqueueMessage.message(text: "", attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil))
// |> map { _ -> Void in
//
// }
// )
}
return signal
}
func callRatingController(account: Account, report: ReportCallRating, present: @escaping (ViewController) -> Void) -> AlertController {
@ -338,13 +350,13 @@ func callRatingController(account: Account, report: ReportCallRating, present: @
if let contentNode = contentNode, let rating = contentNode.rating {
if rating < 4 {
let controller = textAlertController(account: account, title: strings.Call_ReportIncludeLog, text: strings.Call_ReportIncludeLogDescription, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Call_ReportSkip, action: {
rateCallAndSendLogs(account: account, report: report, starsCount: rating, comment: contentNode.comment, includeLogs: false)
let _ = rateCallAndSendLogs(account: account, report: report, starsCount: rating, comment: contentNode.comment, includeLogs: false).start()
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Call_ReportSend, action: {
rateCallAndSendLogs(account: account, report: report, starsCount: rating, comment: contentNode.comment, includeLogs: true)
let _ = rateCallAndSendLogs(account: account, report: report, starsCount: rating, comment: contentNode.comment, includeLogs: true).start()
})])
present(controller)
} else {
rateCallAndSendLogs(account: account, report: report, starsCount: rating, comment: contentNode.comment, includeLogs: false)
let _ = rateCallAndSendLogs(account: account, report: report, starsCount: rating, comment: contentNode.comment, includeLogs: false).start
}
}
})]

View File

@ -177,7 +177,7 @@ final class ChatBotInfoItemNode: ListViewItemNode {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false)
}
func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction {
func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction {
let textNodeFrame = self.textNode.frame
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
if let url = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.URL)] as? String {
@ -206,7 +206,7 @@ final class ChatBotInfoItemNode: ListViewItemNode {
switch recognizer.state {
case .ended:
var foundTapAction = false
let tapAction = self.tapActionAtPoint(recognizer.location(in: self.view))
let tapAction = self.tapActionAtPoint(recognizer.location(in: self.view), gesture: .tap)
switch tapAction {
case .none:
break

View File

@ -7,6 +7,7 @@ import AsyncDisplayKit
import TelegramCore
import SafariServices
import MobileCoreServices
import LegacyComponents
public enum ChatControllerPeekActions {
case standard
@ -3747,11 +3748,12 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
}
private func presentMediaPicker(fileMode: Bool, editingMedia: Bool, completion: @escaping ([Any]) -> Void) {
let _ = (self.account.postbox.transaction { transaction -> GeneratedMediaStoreSettings in
let _ = (self.account.postbox.transaction { transaction -> (GeneratedMediaStoreSettings, SearchBotsConfiguration) in
let entry = transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.generatedMediaStoreSettings) as? GeneratedMediaStoreSettings
return entry ?? GeneratedMediaStoreSettings.defaultSettings
let configuration = currentSearchBotsConfiguration(transaction: transaction)
return (entry ?? GeneratedMediaStoreSettings.defaultSettings, configuration)
}
|> deliverOnMainQueue).start(next: { [weak self] settings in
|> deliverOnMainQueue).start(next: { [weak self] settings, searchBotsConfiguration in
guard let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer else {
return
}
@ -3766,7 +3768,14 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
legacyController.bind(controller: controller)
legacyController.deferScreenEdgeGestures = [.top]
configureLegacyAssetPicker(controller, account: strongSelf.account, peer: peer)
configureLegacyAssetPicker(controller, account: strongSelf.account, peer: peer, presentWebSearch: { [weak self] in
if let strongSelf = self {
let controller = WebSearchController(account: strongSelf.account, chatLocation: .peer(peer.id), configuration: searchBotsConfiguration, sendSelected: { (resuls, collection, editingContext) in
})
strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}
})
controller.descriptionGenerator = legacyAssetPickerItemGenerator()
controller.completionBlock = { [weak legacyController] signals in
if let legacyController = legacyController {
@ -3800,19 +3809,45 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
}
|> deliverOnMainQueue).start(next: { [weak self] configuration in
if let strongSelf = self {
let controller = WebSearchController(account: strongSelf.account, chatLocation: strongSelf.chatLocation, configuration: configuration, sendSelected: { [weak self] ids, collection in
let controller = WebSearchController(account: strongSelf.account, chatLocation: strongSelf.chatLocation, configuration: configuration, sendSelected: { [weak self] ids, collection, editingContext in
if let strongSelf = self {
var results: [ChatContextResult] = []
for id in ids {
var result: ChatContextResult?
for r in collection.results {
if r.id == id {
result = r
results.append(r)
break
}
}
if let result = result {
strongSelf.enqueueChatContextResult(collection, result, includeViaBot: false)
}
if !results.isEmpty {
var signals: [Any] = []
for result in results {
let editableItem = LegacyWebSearchItem(result: result, dimensions: CGSize(), thumbnailImage: .complete(), originalImage: .complete())
if editingContext.adjustments(for: editableItem) != nil {
if let imageSignal = editingContext.imageSignal(for: editableItem) {
let signal = imageSignal.map { image -> Any in
if let image = image as? UIImage {
let dict: [AnyHashable: Any] = [
"type": "editedPhoto",
"image": image
]
return legacyAssetPickerItemGenerator()(dict, nil, nil, nil)
} else {
return SSignal.complete()
}
}
signals.append(signal)
}
} else {
strongSelf.enqueueChatContextResult(collection, result, includeViaBot: false)
}
}
strongSelf.enqueueMediaMessages(signals: signals)
}
}
})
@ -4027,11 +4062,14 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal
}
|> deliverOnMainQueue).start(next: { [weak self] settings in
if let strongSelf = self, let peer = strongSelf.presentationInterfaceState.renderedPeer?.peer {
let controller = legacyPasteMenu(account: strongSelf.account, peer: peer, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, images: images, sendMessagesWithSignals: { signals in
self?.enqueueMediaMessages(signals: signals)
})
strongSelf.chatDisplayNode.dismissInput()
strongSelf.present(controller, in: .window(.root))
let _ = presentLegacyPasteMenu(account: strongSelf.account, peer: peer, saveEditedPhotos: settings.storeEditedPhotos, allowGrouping: true, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, images: images, sendMessagesWithSignals: { signals in
self?.enqueueMediaMessages(signals: signals)
}, present: { [weak self] controller, arguments in
if let strongSelf = self {
strongSelf.present(controller, in: .window(.root), with: arguments)
}
}, initialLayout: strongSelf.validLayout)
}
})
}

View File

@ -544,14 +544,15 @@ final class ChatListNode: ListView {
if previousState.editing != state.editing {
disableAnimations = false
} else {
var previousPinnedCount = 0
var updatedPinnedCount = 0
var previousPinnedChats: [PeerId] = []
var updatedPinnedChats: [PeerId] = []
var didIncludeRemovingPeerId = false
if let previous = previousView {
for entry in previous.filteredEntries {
if case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _) = entry {
if index.pinningIndex != nil {
previousPinnedCount += 1
previousPinnedChats.append(index.messageIndex.id.peerId)
}
if index.messageIndex.id.peerId == removingPeerId {
didIncludeRemovingPeerId = true
@ -563,14 +564,14 @@ final class ChatListNode: ListView {
for entry in processedView.filteredEntries {
if case let .PeerEntry(index, _, _, _, _, _, _, _, _, _, _, _, _) = entry {
if index.pinningIndex != nil {
updatedPinnedCount += 1
updatedPinnedChats.append(index.messageIndex.id.peerId)
}
if index.messageIndex.id.peerId == removingPeerId {
doesIncludeRemovingPeerId = true
}
}
}
if previousPinnedCount != updatedPinnedCount {
if previousPinnedChats != updatedPinnedChats {
disableAnimations = false
}
if previousState.selectedPeerIds != state.selectedPeerIds {
@ -580,6 +581,7 @@ final class ChatListNode: ListView {
disableAnimations = false
}
}
var searchMode = false
if case .peers = mode {

View File

@ -29,16 +29,16 @@ enum UnreadSearchBadge : Equatable {
var count: Int32 {
switch self {
case let .muted(count), let .unmuted(count):
return count
case let .muted(count), let .unmuted(count):
return count
}
}
var isMuted: Bool {
switch self {
case .muted:
return true
case .unmuted:
return false
case .muted:
return true
case .unmuted:
return false
}
}
}
@ -46,6 +46,7 @@ enum UnreadSearchBadge : Equatable {
private struct ChatListSearchRecentPeersEntry: Comparable, Identifiable {
let index: Int
let peer: Peer
let presence: PeerPresence?
let unreadBadge: UnreadSearchBadge?
let itemCustomWidth: CGFloat?
var stableId: PeerId {
@ -62,6 +63,13 @@ private struct ChatListSearchRecentPeersEntry: Comparable, Identifiable {
if !lhs.peer.isEqual(rhs.peer) {
return false
}
if let lhsPresence = lhs.presence, let rhsPresence = rhs.presence {
if !lhsPresence.isEqual(to: rhsPresence) {
return false
}
} else if (lhs.presence != nil) != (rhs.presence != nil) {
return false
}
if lhs.unreadBadge != rhs.unreadBadge {
return false
}
@ -73,7 +81,7 @@ private struct ChatListSearchRecentPeersEntry: Comparable, Identifiable {
}
func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, mode: HorizontalPeerItemMode, peerSelected: @escaping (Peer) -> Void, peerLongTapped: @escaping (Peer) -> Void, isPeerSelected: @escaping (PeerId) -> Bool) -> ListViewItem {
return HorizontalPeerItem(theme: theme, strings: strings, mode: mode, account: account, peer: self.peer, unreadBadge: self.unreadBadge, action: peerSelected, longTapAction: { peer in
return HorizontalPeerItem(theme: theme, strings: strings, mode: mode, account: account, peer: self.peer, presence: self.presence, unreadBadge: self.unreadBadge, action: peerSelected, longTapAction: { peer in
peerLongTapped(peer)
}, isPeerSelected: isPeerSelected, customWidth: itemCustomWidth)
}
@ -114,7 +122,7 @@ final class ChatListSearchRecentPeersNode: ASDisplayNode {
private let isPeerSelected: (PeerId) -> Bool
private let disposable = MetaDisposable()
private let itemCustomWidthValuePromise:ValuePromise<CGFloat?> = ValuePromise(nil, ignoreRepeated: true)
private let itemCustomWidthValuePromise: ValuePromise<CGFloat?> = ValuePromise(nil, ignoreRepeated: true)
private var items: [ListViewItem] = []
private var queuedTransitions: [ChatListSearchRecentNodeTransition] = []
@ -140,8 +148,7 @@ final class ChatListSearchRecentPeersNode: ASDisplayNode {
let peersDisposable = DisposableSet()
let recent: Signal<([Peer], [PeerId : UnreadSearchBadge]), NoError> = recentPeers(account: account)
let recent: Signal<([Peer], [PeerId: UnreadSearchBadge], [PeerId : PeerPresence]), NoError> = recentPeers(account: account)
|> filter { value -> Bool in
switch value {
case .disabled:
@ -151,47 +158,51 @@ final class ChatListSearchRecentPeersNode: ASDisplayNode {
}
} |> mapToSignal { recent in
switch recent {
case .disabled:
return .single(([], [:]))
case let .peers(peers):
return combineLatest(peers.filter { !$0.isDeleted }.map {account.postbox.peerView(id: $0.id)}) |> mapToSignal { peerViews -> Signal<([Peer], [PeerId: UnreadSearchBadge]), NoError> in
return account.postbox.unreadMessageCountsView(items: peerViews.map {.peer($0.peerId)}) |> map { values in
var peers:[Peer] = []
var unread:[PeerId: UnreadSearchBadge] = [:]
for peerView in peerViews {
if let peer = peerViewMainPeer(peerView) {
var isMuted: Bool = false
if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings {
switch notificationSettings.muteState {
case .muted:
isMuted = true
default:
break
case .disabled:
return .single(([], [:], [:]))
case let .peers(peers):
return combineLatest(peers.filter { !$0.isDeleted }.map {account.postbox.peerView(id: $0.id)}) |> mapToSignal { peerViews -> Signal<([Peer], [PeerId: UnreadSearchBadge], [PeerId: PeerPresence]), NoError> in
return account.postbox.unreadMessageCountsView(items: peerViews.map {.peer($0.peerId)}) |> map { values in
var peers: [Peer] = []
var unread: [PeerId: UnreadSearchBadge] = [:]
var presences: [PeerId: PeerPresence] = [:]
for peerView in peerViews {
if let peer = peerViewMainPeer(peerView) {
var isMuted: Bool = false
if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings {
switch notificationSettings.muteState {
case .muted:
isMuted = true
default:
break
}
}
let unreadCount = values.count(for: .peer(peerView.peerId))
if let unreadCount = unreadCount, unreadCount > 0 {
unread[peerView.peerId] = isMuted ? .muted(unreadCount) : .unmuted(unreadCount)
}
if let presence = peerView.peerPresences[peer.id] {
presences[peer.id] = presence
}
peers.append(peer)
}
let unreadCount = values.count(for: .peer(peerView.peerId))
if let unreadCount = unreadCount, unreadCount > 0 {
unread[peerView.peerId] = isMuted ? .muted(unreadCount) : .unmuted(unreadCount)
}
peers.append(peer)
}
return (peers, unread, presences)
}
return (peers, unread)
}
}
}
}
let previous: Atomic<[ChatListSearchRecentPeersEntry]> = Atomic(value: [])
let firstTime:Atomic<Bool> = Atomic(value: true)
peersDisposable.add(combineLatest(recent |> deliverOnMainQueue, itemCustomWidthValuePromise.get() |> deliverOnMainQueue).start(next: { [weak self] peers, itemCustomWidth in
peersDisposable.add((combineLatest(recent, self.itemCustomWidthValuePromise.get()) |> deliverOnMainQueue).start(next: { [weak self] peers, itemCustomWidth in
if let strongSelf = self {
var entries: [ChatListSearchRecentPeersEntry] = []
for peer in peers.0 {
entries.append(ChatListSearchRecentPeersEntry(index: entries.count, peer: peer, unreadBadge: peers.1[peer.id], itemCustomWidth: itemCustomWidth))
entries.append(ChatListSearchRecentPeersEntry(index: entries.count, peer: peer, presence: peers.2[peer.id], unreadBadge: peers.1[peer.id], itemCustomWidth: itemCustomWidth))
}
let animated = !firstTime.swap(false)
@ -265,7 +276,7 @@ final class ChatListSearchRecentPeersNode: ASDisplayNode {
self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: 92.0, height: size.width)
self.listView.position = CGPoint(x: 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: size.width), insets: insets, duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
itemCustomWidthValuePromise.set(itemCustomWidth)
self.itemCustomWidthValuePromise.set(itemCustomWidth)
}
func viewAndPeerAtPoint(_ point: CGPoint) -> (UIView, PeerId)? {

View File

@ -377,7 +377,7 @@ private func universalServiceMessageString(theme: ChatPresentationThemeData?, st
}
attributedString = NSAttributedString(string: titleString, font: titleFont, textColor: primaryTextColor)
case let .customText(text, entities):
attributedString = stringWithAppliedEntities(text, entities: entities, baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: titleFont, linkFont: titleBoldFont, boldFont: titleBoldFont, italicFont: titleFont, fixedFont: titleFont)
attributedString = stringWithAppliedEntities(text, entities: entities, baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: titleFont, linkFont: titleBoldFont, boldFont: titleBoldFont, italicFont: titleFont, fixedFont: titleFont, underlineLinks: false)
case let .botDomainAccessGranted(domain):
attributedString = NSAttributedString(string: strings.AuthSessions_Message(domain).0, font: titleFont, textColor: primaryTextColor)
case let .botSentSecureValues(types):
@ -656,9 +656,9 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
}
}
override func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction {
override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction {
let textNodeFrame = self.labelNode.frame
if let (index, attributes) = self.labelNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY - 10.0)) {
if let (index, attributes) = self.labelNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY - 10.0)), gesture == .tap {
if let url = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.URL)] as? String {
var concealed = true
if let attributeText = self.labelNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {

View File

@ -906,7 +906,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
return false
}
func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction {
func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction {
let textNodeFrame = self.textNode.frame
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
if let url = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.URL)] as? String {

View File

@ -143,7 +143,7 @@ class ChatMessageBubbleContentNode: ASDisplayNode {
func updateAutomaticMediaDownloadSettings(_ settings: AutomaticMediaDownloadSettings) {
}
func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction {
func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction {
return .none
}

View File

@ -221,7 +221,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
return .waitForSingleTap
}
for contentNode in strongSelf.contentNodes {
let tapAction = contentNode.tapActionAtPoint(CGPoint(x: point.x - contentNode.frame.minX, y: point.y - contentNode.frame.minY))
let tapAction = contentNode.tapActionAtPoint(CGPoint(x: point.x - contentNode.frame.minX, y: point.y - contentNode.frame.minY), gesture: .tap)
switch tapAction {
case .none:
break
@ -1570,7 +1570,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
}
var foundTapAction = false
loop: for contentNode in self.contentNodes {
let tapAction = contentNode.tapActionAtPoint(CGPoint(x: location.x - contentNode.frame.minX, y: location.y - contentNode.frame.minY))
let tapAction = contentNode.tapActionAtPoint(CGPoint(x: location.x - contentNode.frame.minX, y: location.y - contentNode.frame.minY), gesture: gesture)
switch tapAction {
case .none, .ignore:
break
@ -1629,7 +1629,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
selectAll = true
}
tapMessage = contentNode.item?.message
let tapAction = contentNode.tapActionAtPoint(CGPoint(x: location.x - contentNode.frame.minX, y: location.y - contentNode.frame.minY))
let tapAction = contentNode.tapActionAtPoint(CGPoint(x: location.x - contentNode.frame.minX, y: location.y - contentNode.frame.minY), gesture: gesture)
switch tapAction {
case .none, .ignore:
break

View File

@ -210,7 +210,7 @@ class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
}
}
override func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction {
override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction {
if self.buttonNode.frame.contains(point) {
return .ignore
} else if self.bounds.contains(point), let item = self.item {

View File

@ -312,7 +312,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
override func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction {
override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction {
if self.buttonNode.frame.contains(point) {
return .openMessage
}

View File

@ -82,7 +82,7 @@ final class ChatMessageEventLogPreviousDescriptionContentNode: ChatMessageBubble
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction {
override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction {
if self.bounds.contains(point) {
/*if let webPage = self.webPage, case let .Loaded(content) = webPage.content {
if content.instantPage != nil {

View File

@ -77,7 +77,7 @@ final class ChatMessageEventLogPreviousLinkContentNode: ChatMessageBubbleContent
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction {
override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction {
if self.bounds.contains(point) {
/*if let webPage = self.webPage, case let .Loaded(content) = webPage.content {
if content.instantPage != nil {

View File

@ -82,10 +82,10 @@ final class ChatMessageEventLogPreviousMessageContentNode: ChatMessageBubbleCont
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction {
override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction {
if self.bounds.contains(point) {
let contentNodeFrame = self.contentNode.frame
return self.contentNode.tapActionAtPoint(point.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY))
return self.contentNode.tapActionAtPoint(point.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY), gesture: gesture)
}
return .none
}

View File

@ -110,7 +110,7 @@ final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction {
override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction {
if self.bounds.contains(point) {
/*if let webPage = self.webPage, case let .Loaded(content) = webPage.content {
if content.instantPage != nil {

View File

@ -449,10 +449,14 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
statusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusSize.width, y: fittedLayoutSize.height - statusSize.height + 10.0), size: statusSize)
}
if let statusFrameValue = statusFrame, descriptionFrame.intersects(statusFrameValue) {
if let statusFrameValue = statusFrame, descriptionFrame.intersects(statusFrameValue) {
fittedLayoutSize.height += 10.0
statusFrame = statusFrameValue.offsetBy(dx: 0.0, dy: 10.0)
}
if let statusFrameValue = statusFrame, let iconFrame = iconFrame, iconFrame.intersects(statusFrameValue) {
fittedLayoutSize.height += 15.0
statusFrame = statusFrameValue.offsetBy(dx: 0.0, dy: 15.0)
}
if isAudio && !isVoice {
streamingCacheStatusFrame = CGRect(origin: CGPoint(x: fittedLayoutSize.width + 6.0, y: 8.0), size: CGSize(width: streamingProgressDiameter, height: streamingProgressDiameter))

View File

@ -111,7 +111,7 @@ final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContentNode {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction {
override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction {
if self.bounds.contains(point) {
/*if let webPage = self.webPage, case let .Loaded(content) = webPage.content {
if content.instantPage != nil {

View File

@ -445,7 +445,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
return mediaHidden
}
override func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction {
override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction {
return .none
}

View File

@ -260,7 +260,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
return mediaHidden
}
override func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction {
override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction {
return .none
}

View File

@ -234,7 +234,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
// }
}
let dateText = stringForMessageTimestampStatus(message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .minimal)
let dateText = stringForMessageTimestampStatus(message: item.message, dateTimeFormat: item.presentationData.dateTimeFormat, nameDisplayOrder: item.presentationData.nameDisplayOrder, strings: item.presentationData.strings, format: .regular)
let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.presentationData.theme, item.presentationData.strings, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude))

View File

@ -277,7 +277,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
self.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
override func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction {
override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction {
let textNodeFrame = self.textNode.frame
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
if let url = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.URL)] as? String {

View File

@ -308,10 +308,10 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
}
override func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction {
override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction {
if self.bounds.contains(point) {
let contentNodeFrame = self.contentNode.frame
let result = self.contentNode.tapActionAtPoint(point.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY))
let result = self.contentNode.tapActionAtPoint(point.offsetBy(dx: -contentNodeFrame.minX, dy: -contentNodeFrame.minY), gesture: gesture)
switch result {
case .none:
break

View File

@ -44,7 +44,7 @@ final class ComposeControllerNode: ASDisplayNode {
ContactListAdditionalOption(title: self.presentationData.strings.Compose_NewChannel, icon: generateTintedImage(image: UIImage(bundleImageName: "Contact List/CreateChannelActionIcon"), color: presentationData.theme.list.itemAccentColor), action: {
openCreateNewChannelImpl?()
})
]))
]), displayPermissionPlaceholder: false)
super.init()

View File

@ -683,11 +683,13 @@ final class ContactListNode: ASDisplayNode {
private var authorizationDisposable: Disposable?
private var authorizationNode: PermissionContentNode
private let displayPermissionPlaceholder: Bool
init(account: Account, presentation: ContactListPresentation, filters: [ContactListFilter] = [.excludeSelf], selectionState: ContactListNodeGroupSelectionState? = nil) {
init(account: Account, presentation: ContactListPresentation, filters: [ContactListFilter] = [.excludeSelf], selectionState: ContactListNodeGroupSelectionState? = nil, displayPermissionPlaceholder: Bool = true) {
self.account = account
self.presentation = presentation
self.filters = filters
self.displayPermissionPlaceholder = displayPermissionPlaceholder
self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
@ -1005,21 +1007,19 @@ final class ContactListNode: ASDisplayNode {
fixSearchableListNodeScrolling(strongSelf.listNode)
}
authorizeImpl = { [weak self] in
if let strongSelf = self {
let _ = (DeviceAccess.authorizationStatus(account: account, subject: .contacts)
|> take(1)
|> deliverOnMainQueue).start(next: { status in
switch status {
case .notDetermined:
DeviceAccess.authorizeAccess(to: .contacts)
case .denied, .restricted:
account.telegramApplicationContext.applicationBindings.openSettings()
default:
break
}
})
}
authorizeImpl = {
let _ = (DeviceAccess.authorizationStatus(account: account, subject: .contacts)
|> take(1)
|> deliverOnMainQueue).start(next: { status in
switch status {
case .notDetermined:
DeviceAccess.authorizeAccess(to: .contacts)
case .denied, .restricted:
account.telegramApplicationContext.applicationBindings.openSettings()
default:
break
}
})
}
openPrivacyPolicyImpl = { [weak self] in
@ -1133,8 +1133,8 @@ final class ContactListNode: ASDisplayNode {
}
})
self.listNode.isHidden = transition.isEmpty
self.authorizationNode.isHidden = !transition.isEmpty
self.listNode.isHidden = self.displayPermissionPlaceholder && transition.isEmpty
self.authorizationNode.isHidden = !transition.isEmpty || !self.displayPermissionPlaceholder
}
}
}

View File

@ -230,20 +230,19 @@ final class GridMessageItemNode: GridItemNode {
self.statusNode.transitionToState(.none, completion: { [weak self] in
self?.statusNode.isHidden = true
})
videoAccessoryNode.isHidden = true
self.videoAccessoryNode.isHidden = true
self.resourceStatus = nil
} else if let file = media as? TelegramMediaFile, file.isVideo {
mediaDimensions = file.dimensions
self.imageNode.setSignal(mediaGridMessageVideo(postbox: account.postbox, videoReference: .message(message: MessageReference(item.message), media: file)))
if let duration = file.duration {
videoAccessoryNode.setup(stringForDuration(duration))
videoAccessoryNode.isHidden = false
self.videoAccessoryNode.setup(stringForDuration(duration))
self.videoAccessoryNode.isHidden = false
} else {
videoAccessoryNode.isHidden = true
self.videoAccessoryNode.isHidden = true
}
self.resourceStatus = nil
self.fetchStatusDisposable.set((messageMediaFileStatus(account: account, messageId: messageId, file: file) |> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self {

View File

@ -22,8 +22,9 @@ final class HorizontalPeerItem: ListViewItem {
let longTapAction: (Peer) -> Void
let isPeerSelected: (PeerId) -> Bool
let customWidth: CGFloat?
let presence: PeerPresence?
let unreadBadge: UnreadSearchBadge?
init(theme: PresentationTheme, strings: PresentationStrings, mode: HorizontalPeerItemMode, account: Account, peer: Peer, unreadBadge: UnreadSearchBadge?, action: @escaping (Peer) -> Void, longTapAction: @escaping (Peer) -> Void, isPeerSelected: @escaping (PeerId) -> Bool, customWidth: CGFloat?) {
init(theme: PresentationTheme, strings: PresentationStrings, mode: HorizontalPeerItemMode, account: Account, peer: Peer, presence: PeerPresence?, unreadBadge: UnreadSearchBadge?, action: @escaping (Peer) -> Void, longTapAction: @escaping (Peer) -> Void, isPeerSelected: @escaping (PeerId) -> Bool, customWidth: CGFloat?) {
self.theme = theme
self.strings = strings
self.mode = mode
@ -33,15 +34,14 @@ final class HorizontalPeerItem: ListViewItem {
self.longTapAction = longTapAction
self.isPeerSelected = isPeerSelected
self.customWidth = customWidth
self.presence = presence
self.unreadBadge = unreadBadge
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = HorizontalPeerItemNode()
let (nodeLayout, apply) = node.asyncLayout()(self, params)
node.insets = nodeLayout.insets
node.contentSize = nodeLayout.contentSize
@ -77,6 +77,7 @@ final class HorizontalPeerItemNode: ListViewItemNode {
private(set) var peerNode: SelectablePeerNode
let badgeBackgroundNode: ASImageNode
let badgeTextNode: TextNode
let statusNode: ASImageNode
private(set) var item: HorizontalPeerItem?
init() {
@ -86,16 +87,22 @@ final class HorizontalPeerItemNode: ListViewItemNode {
self.badgeBackgroundNode.displaysAsynchronously = false
self.badgeBackgroundNode.displayWithoutProcessing = true
self.badgeTextNode = TextNode()
self.badgeTextNode.isUserInteractionEnabled = false
self.badgeTextNode.displaysAsynchronously = true
self.statusNode = ASImageNode()
self.statusNode.isLayerBacked = true
self.statusNode.displaysAsynchronously = false
self.statusNode.displayWithoutProcessing = true
self.statusNode.isHidden = true
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.peerNode)
addSubnode(badgeBackgroundNode)
addSubnode(badgeTextNode)
self.addSubnode(self.badgeBackgroundNode)
self.addSubnode(self.badgeTextNode)
self.addSubnode(self.statusNode)
self.peerNode.toggleSelection = { [weak self] in
if let item = self?.item {
item.action(item.peer)
@ -106,7 +113,6 @@ final class HorizontalPeerItemNode: ListViewItemNode {
item.longTapAction(item.peer)
}
}
}
override func didLoad() {
@ -157,13 +163,28 @@ final class HorizontalPeerItemNode: ListViewItemNode {
badgeAttributedString = NSAttributedString()
}
let currentStatusImage: UIImage?
let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
if let presence = item.presence as? TelegramUserPresence {
let relativeStatus = relativeUserPresenceStatus(presence, relativeTo: Int32(timestamp))
switch relativeStatus {
case .online:
currentStatusImage = PresentationResourcesChatList.recentStatusOnlineIcon(item.theme)
default:
currentStatusImage = nil
}
} else {
currentStatusImage = nil
}
let (badgeLayout, badgeApply) = badgeTextLayout(TextNodeLayoutArguments(attributedString: badgeAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 50.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
var badgeSize: CGFloat = 0.0
if let currentBadgeBackgroundImage = currentBadgeBackgroundImage {
badgeSize += max(currentBadgeBackgroundImage.size.width, badgeLayout.size.width + 10.0) + 5.0
}
return (itemLayout, { animated in
if let strongSelf = self {
strongSelf.item = item
@ -189,6 +210,17 @@ final class HorizontalPeerItemNode: ListViewItemNode {
strongSelf.badgeBackgroundNode.isHidden = true
}
if let currentStatusImage = currentStatusImage {
strongSelf.statusNode.image = currentStatusImage
strongSelf.statusNode.isHidden = false
let statusSize = currentStatusImage.size
strongSelf.statusNode.frame = CGRect(x: itemLayout.size.width - statusSize.width - 18.0, y: itemLayout.size.height - statusSize.height - 18.0, width: statusSize.width, height: statusSize.height)
} else {
strongSelf.statusNode.isHidden = true
}
let _ = badgeApply()
}
})

View File

@ -143,15 +143,18 @@ func legacyMenuPaletteFromTheme(_ theme: PresentationTheme) -> TGMenuSheetPallet
return TGMenuSheetPallete(dark: theme.overallDarkAppearance, backgroundColor: sheetTheme.opaqueItemBackgroundColor, selectionColor: sheetTheme.opaqueItemHighlightedBackgroundColor, separatorColor: sheetTheme.opaqueItemSeparatorColor, accentColor: sheetTheme.controlAccentColor, destructiveColor: sheetTheme.destructiveActionTextColor, textColor: sheetTheme.primaryTextColor, secondaryTextColor: sheetTheme.secondaryTextColor, spinnerColor: sheetTheme.secondaryTextColor, badgeTextColor: sheetTheme.controlAccentColor, badgeImage: nil, cornersImage: generateStretchableFilledCircleImage(diameter: 11.0, color: nil, strokeColor: nil, strokeWidth: nil, backgroundColor: sheetTheme.opaqueItemBackgroundColor))
}
func legacyPasteMenu(account: Account, peer: Peer, saveEditedPhotos: Bool, allowGrouping: Bool, theme: PresentationTheme, strings: PresentationStrings, images: [UIImage], sendMessagesWithSignals: @escaping ([Any]?) -> Void) -> ViewController {
let legacyController = LegacyController(presentation: .custom, theme: theme)
legacyController.statusBar.statusBarStyle = .Hide
func presentLegacyPasteMenu(account: Account, peer: Peer, saveEditedPhotos: Bool, allowGrouping: Bool, theme: PresentationTheme, strings: PresentationStrings, images: [UIImage], sendMessagesWithSignals: @escaping ([Any]?) -> Void, present: (ViewController, Any?) -> Void, initialLayout: ContainerViewLayout? = nil) -> ViewController {
let legacyController = LegacyController(presentation: .custom, theme: theme, initialLayout: initialLayout)
legacyController.statusBar.statusBarStyle = .Ignore
legacyController.controllerLoaded = { [weak legacyController] in
legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true
}
let baseController = TGViewController(context: legacyController.context)!
legacyController.bind(controller: baseController)
let emptyController = LegacyEmptyController(context: legacyController.context)!
let navigationController = makeLegacyNavigationController(rootController: emptyController)
navigationController.setNavigationBarHidden(true, animated: false)
legacyController.bind(controller: navigationController)
var hasTimer = false
if (peer is TelegramUser) && peer.id != account.peerId {
hasTimer = true
@ -159,20 +162,15 @@ func legacyPasteMenu(account: Account, peer: Peer, saveEditedPhotos: Bool, allow
let recipientName = peer.displayTitle
legacyController.enableSizeClassSignal = true
legacyController.presentationCompleted = { [weak legacyController, weak baseController] in
if let strongLegacyController = legacyController, let baseController = baseController {
let controller = TGClipboardMenu.present(inParentController: baseController, context: strongLegacyController.context, images: images, hasCaption: true, hasTimer: hasTimer, recipientName: recipientName, completed: { selectionContext, editingContext, currentItem in
let signals = TGClipboardMenu.resultSignals(for: selectionContext, editingContext: editingContext, currentItem: currentItem, descriptionGenerator: legacyAssetPickerItemGenerator())
sendMessagesWithSignals(signals)
}, dismissed: {
if let strongLegacyController = legacyController {
strongLegacyController.dismiss()
}
}, sourceView: baseController.view, sourceRect: nil)
controller?.customRemoveFromParentViewController = { [weak legacyController] in
legacyController?.dismiss()
}
}
let controller = TGClipboardMenu.present(inParentController: emptyController, context: legacyController.context, images: images, hasCaption: true, hasTimer: hasTimer, recipientName: recipientName, completed: { selectionContext, editingContext, currentItem in
let signals = TGClipboardMenu.resultSignals(for: selectionContext, editingContext: editingContext, currentItem: currentItem, descriptionGenerator: legacyAssetPickerItemGenerator())
sendMessagesWithSignals(signals)
}, dismissed: { [weak legacyController] in
legacyController?.dismiss()
}, sourceView: emptyController.view, sourceRect: nil)!
controller.customRemoveFromParentViewController = { [weak legacyController] in
legacyController?.dismiss()
}
let presentationDisposable = account.telegramApplicationContext.presentationData.start(next: { [weak legacyController] presentationData in
@ -182,5 +180,8 @@ func legacyPasteMenu(account: Account, peer: Peer, saveEditedPhotos: Bool, allow
})
legacyController.disposables.add(presentationDisposable)
present(legacyController, nil)
controller.present(in: emptyController, sourceView: nil, animated: true)
return legacyController
}

View File

@ -13,7 +13,7 @@ func guessMimeTypeByFileExtension(_ ext: String) -> String {
return TGMimeTypeMap.mimeType(forExtension: ext) ?? "application/binary"
}
func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, account: Account, peer: Peer, captionsEnabled: Bool = true, storeCreatedAssets: Bool = true, showFileTooltip: Bool = false) {
func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, account: Account, peer: Peer, captionsEnabled: Bool = true, storeCreatedAssets: Bool = true, showFileTooltip: Bool = false, presentWebSearch: (() -> Void)?) {
controller.captionsEnabled = captionsEnabled
controller.inhibitDocumentCaptions = false
controller.suggestionContext = legacySuggestionContext(account: account, peerId: peer.id)
@ -21,11 +21,11 @@ func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, account:
controller.hasTimer = true
}
controller.dismissalBlock = {
}
controller.localMediaCacheEnabled = false
controller.shouldStoreAssets = storeCreatedAssets
controller.shouldShowFileTipIfNeeded = showFileTooltip
controller.requestSearchController = presentWebSearch
}
func legacyAssetPicker(applicationContext: TelegramApplicationContext, presentationData: PresentationData, editingMedia: Bool, fileMode: Bool, peer: Peer?, saveEditedPhotos: Bool, allowGrouping: Bool) -> Signal<(LegacyComponentsContext) -> TGMediaAssetsController, Void> {

View File

@ -0,0 +1,329 @@
import Foundation
import LegacyComponents
import SwiftSignalKit
import TelegramCore
import Postbox
import SSignalKit
import UIKit
import Display
class LegacyWebSearchItem: NSObject, TGMediaEditableItem, TGMediaSelectableItem {
var isVideo: Bool {
return false
}
var uniqueIdentifier: String! {
return self.result.id
}
let result: ChatContextResult
let dimensions: CGSize
let thumbnailImage: Signal<UIImage, NoError>
let originalImage: Signal<UIImage, NoError>
init(result: ChatContextResult, dimensions: CGSize, thumbnailImage: Signal<UIImage, NoError>, originalImage: Signal<UIImage, NoError>) {
self.result = result
self.dimensions = dimensions
self.thumbnailImage = thumbnailImage
self.originalImage = originalImage
}
var originalSize: CGSize {
return self.dimensions
}
func thumbnailImageSignal() -> SSignal! {
return SSignal(generator: { subscriber -> SDisposable? in
let disposable = self.thumbnailImage.start(next: { image in
subscriber?.putNext(image)
subscriber?.putCompletion()
})
return SBlockDisposable(block: {
disposable.dispose()
})
})
}
func screenImageSignal(_ position: TimeInterval) -> SSignal! {
return self.originalImageSignal(position)
}
func originalImageSignal(_ position: TimeInterval) -> SSignal! {
return SSignal(generator: { subscriber -> SDisposable? in
let disposable = self.originalImage.start(next: { image in
subscriber?.putNext(image)
subscriber?.putCompletion()
})
return SBlockDisposable(block: {
disposable.dispose()
})
})
}
}
private class LegacyWebSearchGalleryItem: TGModernGalleryImageItem, TGModernGalleryEditableItem, TGModernGallerySelectableItem {
var selectionContext: TGMediaSelectionContext!
var editingContext: TGMediaEditingContext!
let item: LegacyWebSearchItem
init(item: LegacyWebSearchItem) {
self.item = item
super.init()
}
func editableMediaItem() -> TGMediaEditableItem! {
return self.item
}
func selectableMediaItem() -> TGMediaSelectableItem! {
return self.item
}
func toolbarTabs() -> TGPhotoEditorTab {
return [.cropTab, .paintTab, .toolsTab]
}
func uniqueId() -> String! {
return self.item.uniqueIdentifier
}
override func viewClass() -> AnyClass! {
return LegacyWebSearchGalleryItemView.self
}
}
private class LegacyWebSearchGalleryItemView: TGModernGalleryImageItemView, TGModernGalleryEditableItemView
{
func setHiddenAsBeingEdited(_ hidden: Bool) {
self.imageView.isHidden = hidden
}
override func setItem(_ item: TGModernGalleryItem!, synchronously: Bool) {
if let item = item as? LegacyWebSearchGalleryItem {
self._setItem(item)
self.imageSize = TGFitSize(item.editableMediaItem().originalSize!, CGSize(width: 1600, height: 1600))
let signal = item.editingContext.imageSignal(for: item.editableMediaItem())?.map(toSignal: { result -> SSignal? in
if let image = result as? UIImage {
return SSignal.single(image)
} else if result == nil, let mediaItem = item.editableMediaItem() as? LegacyWebSearchItem {
return mediaItem.originalImageSignal(0.0)
} else {
return SSignal.complete()
}
})
self.imageView.setSignal(signal?.deliver(on: SQueue.main())?.afterNext({ [weak self] next in
if let strongSelf = self, let image = next as? UIImage {
strongSelf.imageSize = image.size
strongSelf.reset()
}
}))
self.reset()
} else {
self.imageView.setSignal(nil)
super.setItem(item, synchronously: synchronously)
}
}
override func contentView() -> UIView! {
return self.imageView
}
override func transitionContentView() -> UIView! {
return self.contentView()
}
override func transitionViewContentRect() -> CGRect {
let contentView = self.transitionContentView()!
return contentView.convert(contentView.bounds, to: self.transitionView())
}
}
private func galleryItems(account: Account, results: [ChatContextResult], current: ChatContextResult, selectionContext: TGMediaSelectionContext, editingContext: TGMediaEditingContext) -> ([TGModernGalleryItem], TGModernGalleryItem?) {
var focusItem: TGModernGalleryItem?
var galleryItems: [TGModernGalleryItem] = []
for result in results {
var thumbnailDimensions: CGSize?
var thumbnailResource: TelegramMediaResource?
var imageResource: TelegramMediaResource?
var imageDimensions = CGSize()
let thumbnailSignal: Signal<UIImage, NoError>
let originalSignal: Signal<UIImage, NoError>
switch result {
case let .externalReference(_, _, _, _, _, _, content, thumbnail, _):
if let content = content {
imageResource = content.resource
}
if let thumbnail = thumbnail {
thumbnailResource = thumbnail.resource
thumbnailDimensions = thumbnail.dimensions
}
if let dimensions = content?.dimensions {
imageDimensions = dimensions
}
// if let imageResource = imageResource {
// updatedStatusSignal = item.account.postbox.mediaBox.resourceStatus(imageResource)
// }
case let .internalReference(_, _, _, _, _, image, _, _):
if let image = image {
if let largestRepresentation = largestImageRepresentation(image.representations) {
imageDimensions = largestRepresentation.dimensions
imageResource = imageRepresentationLargerThan(image.representations, size: CGSize(width: 1000.0, height: 800.0))?.resource
}
if let thumbnailRepresentation = imageRepresentationLargerThan(image.representations, size: CGSize(width: 200.0, height: 100.0)) {
thumbnailDimensions = thumbnailRepresentation.dimensions
thumbnailResource = thumbnailRepresentation.resource
}
}
}
if let imageResource = imageResource {
var representations: [TelegramMediaImageRepresentation] = []
if let thumbnailResource = thumbnailResource, let thumbnailDimensions = thumbnailDimensions {
representations.append(TelegramMediaImageRepresentation(dimensions: thumbnailDimensions, resource: thumbnailResource))
}
representations.append(TelegramMediaImageRepresentation(dimensions: imageDimensions, resource: imageResource))
let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, reference: nil, partialReference: nil)
thumbnailSignal = chatMessagePhotoDatas(postbox: account.postbox, photoReference: .standalone(media: tmpImage), autoFetchFullSize: true)
|> mapToSignal { (thumbnailData, fullSizeData, fullSizeComplete) -> Signal<UIImage, NoError> in
if let data = fullSizeData, let image = UIImage(data: data) {
return .single(image)
} else {
return .complete()
}
}
originalSignal = thumbnailSignal
let item = LegacyWebSearchItem(result: result, dimensions: imageDimensions, thumbnailImage: thumbnailSignal, originalImage: originalSignal)
let galleryItem = LegacyWebSearchGalleryItem(item: item)
galleryItem.selectionContext = selectionContext
galleryItem.editingContext = editingContext
if result.id == current.id {
focusItem = galleryItem
}
galleryItems.append(galleryItem)
}
}
return (galleryItems, focusItem)
}
func presentLegacyWebSearchGallery(account: Account, peer: Peer?, theme: PresentationTheme, results: [ChatContextResult], current: ChatContextResult, selectionContext: TGMediaSelectionContext, editingContext: TGMediaEditingContext, updateHiddenMedia: @escaping (String?) -> Void, transitionHostView: @escaping () -> UIView?, transitionView: @escaping (ChatContextResult) -> UIView?, completed: @escaping (ChatContextResult) -> Void, present: (ViewController, Any?) -> Void) {
let legacyController = LegacyController(presentation: .custom, theme: theme)
legacyController.statusBar.statusBarStyle = .Ignore
let controller = TGModernGalleryController(context: legacyController.context)!
legacyController.bind(controller: controller)
let (items, focusItem) = galleryItems(account: account, results: results, current: current, selectionContext: selectionContext, editingContext: editingContext)
let model = TGMediaPickerGalleryModel(context: legacyController.context, items: items, focus: focusItem, selectionContext: selectionContext, editingContext: editingContext, hasCaptions: true, allowCaptionEntities: true, hasTimer: false, onlyCrop: false, inhibitDocumentCaptions: false, hasSelectionPanel: false, hasCamera: false, recipientName: peer?.displayTitle)!
if let peer = peer {
model.suggestionContext = legacySuggestionContext(account: account, peerId: peer.id)
}
controller.model = model
model.controller = controller
model.externalSelectionCount = {
return 0
}
model.useGalleryImageAsEditableItemImage = true
model.storeOriginalImageForItem = { item, image in
editingContext.setOriginalImage(image, for: item, synchronous: false)
}
model.willFinishEditingItem = { item, adjustments, representation, hasChanges in
if hasChanges {
editingContext.setAdjustments(adjustments, for: item)
}
editingContext.setTemporaryRep(representation, for: item)
//if let selectionContext = selectionContext, {
// selectionContex
//}
}
model.didFinishEditingItem = { item, adjustments, result, thumbnail in
editingContext.setImage(result, thumbnailImage: thumbnail, for: item, synchronous: true)
}
model.saveItemCaption = { item, caption, entities in
editingContext.setCaption(caption, entities: entities, for: item)
}
//[model.interfaceView updateSelectionInterface:[self totalSelectionCount] counterVisible:([self totalSelectionCount] > 0) animated:false];
model.interfaceView.donePressed = { item in
if let item = item as? LegacyWebSearchGalleryItem {
controller.dismissWhenReady(animated: false)
completed(item.item.result)
}
}
controller.transitionHost = {
return transitionHostView()
}
controller.itemFocused = { item in
if let item = item as? LegacyWebSearchGalleryItem {
updateHiddenMedia(item.item.result.id)
}
}
controller.beginTransitionIn = { item, _ in
if let item = item as? LegacyWebSearchGalleryItem {
return transitionView(item.item.result)
} else {
return nil
}
}
controller.beginTransitionOut = { item, _ in
if let item = item as? LegacyWebSearchGalleryItem {
return transitionView(item.item.result)
} else {
return nil
}
}
controller.completedTransitionOut = { [weak legacyController] in
updateHiddenMedia(nil)
legacyController?.dismiss()
}
present(legacyController, nil)
// if (item.selectionContext != nil && adjustments != nil && [editableItem conformsToProtocol:@protocol(TGMediaSelectableItem)])
// [item.selectionContext setItem:(id<TGMediaSelectableItem>)editableItem selected:true];
// };
// model.interfaceView.donePressed = ^(id<TGWebSearchResultsGalleryItem> item)
// {
// __strong TGWebSearchController *strongSelf = weakSelf;
// if (strongSelf == nil)
// return;
//
// NSMutableArray *selectedItems = [strongSelf selectedItems];
//
// if (selectedItems.count == 0)
// [selectedItems addObject:[item webSearchResult]];
//
// strongSelf->_selectedItems = selectedItems;
// [strongSelf complete];
// };
// _galleryModel = model;
// modernGallery.model = model;
//
// __weak TGModernGalleryController *weakGallery = modernGallery;
// modernGallery.itemFocused = ^(id<TGWebSearchResultsGalleryItem> item)
// {
// __strong TGWebSearchController *strongSelf = weakSelf;
// __strong TGModernGalleryController *strongGallery = weakGallery;
// if (strongSelf != nil)
// {
// if (strongGallery.previewMode)
// return;
//
// id<TGWebSearchListItem> listItem = [strongSelf listItemForSearchResult:[item webSearchResult]];
// strongSelf->_hiddenItem = listItem;
// [strongSelf updateHiddenItemAnimated:false];
// }
// };
//
}

View File

@ -173,7 +173,7 @@ final class ListMessageFileItemNode: ListMessageNode {
private var appliedItem: ListMessageItem?
private var layoutParams: ListViewItemLayoutParams?
private var contentSizeValue: CGSize?
private var currentLeftOffet: CGFloat = 0.0
private var currentLeftOffset: CGFloat = 0.0
override var canBeLongTapped: Bool {
return true
@ -305,7 +305,7 @@ final class ListMessageFileItemNode: ListMessageNode {
updatedTheme = item.theme
}
var leftInset: CGFloat = 60.0 + params.leftInset
var leftInset: CGFloat = 65.0 + params.leftInset
let rightInset: CGFloat = 8.0 + params.rightInset
var leftOffset: CGFloat = 0.0
@ -483,7 +483,7 @@ final class ListMessageFileItemNode: ListMessageNode {
insets.top += header.height
}
let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: isAudio ? 56.0 : 52.0), insets: insets)
let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 56.0), insets: insets)
return (nodeLayout, { animation in
if let strongSelf = self {
@ -500,7 +500,7 @@ final class ListMessageFileItemNode: ListMessageNode {
strongSelf.appliedItem = item
strongSelf.layoutParams = params
strongSelf.contentSizeValue = nodeLayout.contentSize
strongSelf.currentLeftOffet = leftOffset
strongSelf.currentLeftOffset = leftOffset
if let _ = updatedTheme {
strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor
@ -508,7 +508,6 @@ final class ListMessageFileItemNode: ListMessageNode {
strongSelf.progressNode.updateTheme(RadialProgressTheme(backgroundColor: item.theme.list.itemAccentColor, foregroundColor: item.theme.list.plainBackgroundColor, icon: nil))
strongSelf.linearProgressNode.backgroundColor = item.theme.list.itemAccentColor
}
if let (selectionWidth, selectionApply) = selectionNodeWidthAndApply {
@ -552,7 +551,7 @@ final class ListMessageFileItemNode: ListMessageNode {
}
}
transition.updateFrame(node: strongSelf.descriptionNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset + descriptionOffset, y: isAudio ? 32.0 : 29.0), size: descriptionNodeLayout.size))
transition.updateFrame(node: strongSelf.descriptionNode, frame: CGRect(origin: CGPoint(x: leftOffset + leftInset + descriptionOffset, y: 32.0), size: descriptionNodeLayout.size))
let _ = descriptionNodeApply()
let iconFrame: CGRect
@ -565,7 +564,7 @@ final class ListMessageFileItemNode: ListMessageNode {
}
transition.updateFrame(node: strongSelf.extensionIconNode, frame: iconFrame)
strongSelf.extensionIconNode.image = extensionIconImage
transition.updateFrame(node: strongSelf.extensionIconText, frame: CGRect(origin: CGPoint(x: leftOffset + 9.0 + floor((42.0 - extensionTextLayout.size.width) / 2.0), y: 5.0 + floor((42.0 - extensionTextLayout.size.height) / 2.0)), size: extensionTextLayout.size))
transition.updateFrame(node: strongSelf.extensionIconText, frame: CGRect(origin: CGPoint(x: leftOffset + 12.0 + floor((42.0 - extensionTextLayout.size.width) / 2.0), y: 5.0 + floor((42.0 - extensionTextLayout.size.height) / 2.0)), size: extensionTextLayout.size))
let _ = extensionTextApply()
@ -787,7 +786,7 @@ final class ListMessageFileItemNode: ListMessageNode {
switch maybeFetchStatus {
case let .Fetching(_, progress):
let progressFrame = CGRect(x: self.currentLeftOffet + leftInset + 65.0, y: size.height - 2.0, width: floor((size.width - 65.0 - leftInset - rightInset) * CGFloat(progress)), height: 2.0)
let progressFrame = CGRect(x: self.currentLeftOffset + leftInset + 65.0, y: size.height - 2.0, width: floor((size.width - 65.0 - leftInset - rightInset) * CGFloat(progress)), height: 2.0)
if self.linearProgressNode.supernode == nil {
self.addSubnode(self.linearProgressNode)
}

View File

@ -8,6 +8,7 @@ import TelegramCore
enum NativeVideoContentId: Hashable {
case message(MessageId, UInt32, MediaId)
case instantPage(MediaId, MediaId)
case contextResult(Int64, String)
static func ==(lhs: NativeVideoContentId, rhs: NativeVideoContentId) -> Bool {
switch lhs {
@ -23,6 +24,12 @@ enum NativeVideoContentId: Hashable {
} else {
return false
}
case let .contextResult(queryId, resultId):
if case .contextResult(queryId, resultId) = rhs {
return true
} else {
return false
}
}
}
@ -32,6 +39,8 @@ enum NativeVideoContentId: Hashable {
return messageId.hashValue &* 31 &+ mediaId.hashValue
case let .instantPage(pageId, mediaId):
return pageId.hashValue &* 31 &+ mediaId.hashValue
case let .contextResult(queryId, resultId):
return queryId.hashValue &* 31 &+ resultId.hashValue
}
}
}

View File

@ -43,10 +43,12 @@ private func chatMessageGalleryControllerData(account: Account, message: Message
} else if let image = media as? TelegramMediaImage {
galleryMedia = image
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, !excludeWebPageMedia {
if let file = content.file {
galleryMedia = file
} else if let image = content.image {
galleryMedia = image
if ["photo", "document", "video", "gif", "audio", "voice"].contains(content.type) {
if let file = content.file {
galleryMedia = file
} else if let image = content.image {
galleryMedia = image
}
}
if let instantPage = content.instantPage, let galleryMedia = galleryMedia {
switch instantPageType(of: content) {

View File

@ -136,7 +136,7 @@ func openResolvedUrl(_ resolvedUrl: ResolvedUrl, account: Account, context: Open
let textInputState: ChatTextInputState
if let text = text, !text.isEmpty {
let urlString = NSMutableAttributedString(string: "\(text)\n")
let urlString = NSMutableAttributedString(string: "\(url)\n")
let textString = NSAttributedString(string: "\(text)")
let selectionRange: Range<Int> = urlString.length ..< (urlString.length + textString.length)
urlString.append(textString)

View File

@ -115,6 +115,9 @@ public class PeerMediaCollectionController: TelegramController {
}
}, openPeerMention: { _ in
}, openMessageContextMenu: { [weak self] message, _, _, _ in
var messageIds = Set<MessageId>()
messageIds.insert(message.id)
if let strongSelf = self, strongSelf.isNodeLoaded {
if let message = strongSelf.mediaCollectionDisplayNode.messageForGallery(message.id)?.message {
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
@ -124,6 +127,18 @@ public class PeerMediaCollectionController: TelegramController {
if let strongSelf = self, let navigationController = strongSelf.navigationController as? NavigationController {
navigateToChatController(navigationController: navigationController, account: strongSelf.account, chatLocation: .peer(strongSelf.peerId), messageId: message.id)
}
}),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_ContextMenuForward, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let strongSelf = self {
strongSelf.forwardMessages(messageIds)
}
}),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_ContextMenuDelete, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let strongSelf = self {
strongSelf.deleteMessages(messageIds)
}
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
@ -244,63 +259,8 @@ public class PeerMediaCollectionController: TelegramController {
}, setupEditMessage: { _ in
}, beginMessageSelection: { _ in
}, deleteSelectedMessages: { [weak self] in
if let strongSelf = self {
if let messageIds = strongSelf.interfaceState.selectionState?.selectedIds, !messageIds.isEmpty {
strongSelf.messageContextDisposable.set((combineLatest(chatAvailableMessageActions(postbox: strongSelf.account.postbox, accountPeerId: strongSelf.account.peerId, messageIds: messageIds), strongSelf.peer.get() |> take(1)) |> deliverOnMainQueue).start(next: { actions, peer in
if let strongSelf = self, let peer = peer, !actions.options.isEmpty {
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
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 actions.options.contains(.deleteGlobally) {
let globalTitle: String
if isChannel {
globalTitle = strongSelf.presentationData.strings.Conversation_DeleteMessagesForMe
} else if let personalPeerName = personalPeerName {
globalTitle = strongSelf.presentationData.strings.Conversation_DeleteMessagesFor(personalPeerName).0
} else {
globalTitle = strongSelf.presentationData.strings.Conversation_DeleteMessagesForEveryone
}
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 actions.options.contains(.deleteLocally) {
var localOptionText = strongSelf.presentationData.strings.Conversation_DeleteMessagesForMe
if strongSelf.account.peerId == strongSelf.peerId {
if messageIds.count == 1 {
localOptionText = strongSelf.presentationData.strings.Conversation_Moderate_Delete
} else {
localOptionText = strongSelf.presentationData.strings.Conversation_DeleteManyMessages
}
}
items.append(ActionSheetButtonItem(title: localOptionText, 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: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
strongSelf.present(actionSheet, in: .window(.root))
}
}))
}
if let strongSelf = self, let messageIds = strongSelf.interfaceState.selectionState?.selectedIds {
strongSelf.deleteMessages(messageIds)
}
}, reportSelectedMessages: { [weak self] in
if let strongSelf = self, let messageIds = strongSelf.interfaceState.selectionState?.selectedIds, !messageIds.isEmpty {
@ -313,37 +273,7 @@ public class PeerMediaCollectionController: TelegramController {
}, 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.transaction({ transaction -> Void in
transaction.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<Bool>()
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, chatLocation: .peer(peerId)), animated: false, ready: ready)
}
})
}
}
strongSelf.present(controller, in: .window(.root))
strongSelf.forwardMessages(forwardMessageIdsSet)
}
}
}, forwardMessages: { _ in
@ -646,4 +576,97 @@ public class PeerMediaCollectionController: TelegramController {
}
}))
}
func forwardMessages(_ messageIds: Set<MessageId>) {
let forwardMessageIds = Array(messageIds).sorted()
let controller = PeerSelectionController(account: self.account)
controller.peerSelected = { [weak self, weak controller] peerId in
if let strongSelf = self, let _ = controller {
let _ = (strongSelf.account.postbox.transaction({ transaction -> Void in
transaction.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<Bool>()
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, chatLocation: .peer(peerId)), animated: false, ready: ready)
}
})
}
}
self.present(controller, in: .window(.root))
}
func deleteMessages(_ messageIds: Set<MessageId>) {
if !messageIds.isEmpty {
self.messageContextDisposable.set((combineLatest(chatAvailableMessageActions(postbox: self.account.postbox, accountPeerId: self.account.peerId, messageIds: messageIds), self.peer.get() |> take(1)) |> deliverOnMainQueue).start(next: { [weak self] actions, peer in
if let strongSelf = self, let peer = peer, !actions.options.isEmpty {
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
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 actions.options.contains(.deleteGlobally) {
let globalTitle: String
if isChannel {
globalTitle = strongSelf.presentationData.strings.Conversation_DeleteMessagesForMe
} else if let personalPeerName = personalPeerName {
globalTitle = strongSelf.presentationData.strings.Conversation_DeleteMessagesFor(personalPeerName).0
} else {
globalTitle = strongSelf.presentationData.strings.Conversation_DeleteMessagesForEveryone
}
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 actions.options.contains(.deleteLocally) {
var localOptionText = strongSelf.presentationData.strings.Conversation_DeleteMessagesForMe
if strongSelf.account.peerId == strongSelf.peerId {
if messageIds.count == 1 {
localOptionText = strongSelf.presentationData.strings.Conversation_Moderate_Delete
} else {
localOptionText = strongSelf.presentationData.strings.Conversation_DeleteManyMessages
}
}
items.append(ActionSheetButtonItem(title: localOptionText, 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: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
strongSelf.present(actionSheet, in: .window(.root))
}
}))
}
}
}

View File

@ -104,7 +104,8 @@ class PermissionInfoItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let closeButton: HighlightableButtonNode
let badgeNode: ASImageNode
let labelNode: TextNode
let titleNode: TextNode
@ -140,6 +141,10 @@ class PermissionInfoItemNode: ListViewItemNode {
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
self.closeButton = HighlightableButtonNode()
self.closeButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0)
self.closeButton.displaysAsynchronously = false
super.init(layerBacked: false, dynamicBounce: false)
@ -147,6 +152,7 @@ class PermissionInfoItemNode: ListViewItemNode {
self.addSubnode(self.labelNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.closeButton)
}
func asyncLayout() -> (_ item: PermissionInfoItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors?) -> (ListViewItemNodeLayout, () -> Void) {
@ -163,10 +169,13 @@ class PermissionInfoItemNode: ListViewItemNode {
var updatedTheme: PresentationTheme?
var updatedBadgeImage: UIImage?
var updatedCloseIcon: UIImage?
let badgeDiameter: CGFloat = 20.0
if currentItem?.theme !== item.theme {
updatedTheme = item.theme
updatedBadgeImage = generateStretchableFilledCircleImage(diameter: badgeDiameter, color: item.theme.list.itemDestructiveColor)
updatedCloseIcon = PresentationResourcesItemList.itemListCloseIconImage(item.theme)
}
let insets: UIEdgeInsets
@ -256,6 +265,15 @@ class PermissionInfoItemNode: ListViewItemNode {
bottomStripeInset = leftInset
}
if let item = strongSelf.item {
switch (item.subject, item.type) {
case (.contacts, _), (.notifications, .unreachable):
strongSelf.closeButton.isHidden = false
default:
strongSelf.closeButton.isHidden = true
}
}
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.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: params.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
@ -267,6 +285,10 @@ class PermissionInfoItemNode: ListViewItemNode {
strongSelf.badgeNode.image = updateBadgeImage
}
if let updatedCloseIcon = updatedCloseIcon {
strongSelf.closeButton.setImage(updatedCloseIcon, for: [])
}
strongSelf.badgeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 16.0), size: CGSize(width: badgeDiameter, height: badgeDiameter))
strongSelf.labelNode.frame = CGRect(origin: CGPoint(x: strongSelf.badgeNode.frame.midX - labelLayout.size.width / 2.0, y: strongSelf.badgeNode.frame.minY + 1.0), size: labelLayout.size)
@ -274,6 +296,8 @@ class PermissionInfoItemNode: ListViewItemNode {
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: strongSelf.badgeNode.frame.maxX + 8.0, y: 16.0), size: titleLayout.size)
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: strongSelf.titleNode.frame.maxY + 9.0), size: textLayout.size)
strongSelf.closeButton.frame = CGRect(x: params.width - rightInset - 20.0, y: 2.0, width: 32.0, height: 32.0)
}
})
}

View File

@ -16,7 +16,7 @@ public func largestRepresentationForPhoto(_ photo: TelegramMediaImage) -> Telegr
return photo.representationForDisplayAtSize(CGSize(width: 1280.0, height: 1280.0))
}
private func chatMessagePhotoDatas(postbox: Postbox, photoReference: ImageMediaReference, fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false, tryAdditionalRepresentations: Bool = false, synchronousLoad: Bool = false) -> Signal<(Data?, Data?, Bool), NoError> {
func chatMessagePhotoDatas(postbox: Postbox, photoReference: ImageMediaReference, fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false, tryAdditionalRepresentations: Bool = false, synchronousLoad: Bool = false) -> Signal<(Data?, Data?, Bool), NoError> {
if let smallestRepresentation = smallestImageRepresentation(photoReference.media.representations), let largestRepresentation = photoReference.media.representationForDisplayAtSize(fullRepresentationSize) {
let maybeFullSize = postbox.mediaBox.resourceData(largestRepresentation.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad)

View File

@ -52,6 +52,8 @@ enum PresentationResourceKey: Int32 {
case itemListCloudFetchIcon
case itemListCloseIconImage
case chatListLockTopLockedImage
case chatListLockBottomLockedImage
case chatListLockTopUnlockedImage
@ -66,6 +68,7 @@ enum PresentationResourceKey: Int32 {
case chatListMutedIcon
case chatListVerifiedIcon
case chatListSecretIcon
case chatListRecentStatusOnlineIcon
case chatTitleLockIcon
case chatTitleMuteIcon

View File

@ -111,6 +111,19 @@ struct PresentationResourcesChatList {
})
}
static func recentStatusOnlineIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatListRecentStatusOnlineIcon.rawValue, { theme in
return generateImage(CGSize(width: 14.0, height: 14.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.setFillColor(theme.chatList.backgroundColor.cgColor)
context.fillEllipse(in: bounds)
context.setFillColor(theme.chatList.unreadBadgeActiveBackgroundColor.cgColor)
context.fillEllipse(in: bounds.insetBy(dx: 2.0, dy: 2.0))
})
})
}
static func badgeBackgroundActive(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatListBadgeBackgroundActive.rawValue, { theme in
return generateBadgeBackgroundImage(theme: theme, active: true)

View File

@ -135,4 +135,22 @@ struct PresentationResourcesItemList {
generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/FileCloudFetch"), color: theme.list.itemAccentColor)
})
}
static func itemListCloseIconImage(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.itemListCloseIconImage.rawValue, { theme in
return generateImage(CGSize(width: 12.0, height: 12.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.copy)
context.setStrokeColor(theme.list.disclosureArrowColor.cgColor)
context.setLineWidth(2.0)
context.setLineCap(.round)
context.move(to: CGPoint(x: 1.0, y: 1.0))
context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - 1.0))
context.strokePath()
context.move(to: CGPoint(x: size.width - 1.0, y: 1.0))
context.addLine(to: CGPoint(x: 1.0, y: size.height - 1.0))
context.strokePath()
})
})
}
}

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@ import SwiftSignalKit
import Display
import AsyncDisplayKit
import TelegramCore
import LegacyComponents
private func requestContextResults(account: Account, botId: PeerId, query: String, peerId: PeerId, offset: String = "", existingResults: ChatContextResultCollection? = nil, limit: Int = 30) -> Signal<ChatContextResultCollection?, NoError> {
return requestChatContextResults(account: account, botId: botId, peerId: peerId, query: query, offset: offset)
@ -30,15 +31,18 @@ final class WebSearchControllerInteraction {
let setSearchQuery: (String) -> Void
let deleteRecentQuery: (String) -> Void
let toggleSelection: ([String], Bool) -> Void
let sendSelected: (ChatContextResultCollection) -> Void
let sendSelected: (ChatContextResultCollection, ChatContextResult?) -> Void
var selectionState: WebSearchSelectionState?
var hiddenMediaId: String?
let editingContext: TGMediaEditingContext
init(openResult: @escaping (ChatContextResult) -> Void, setSearchQuery: @escaping (String) -> Void, deleteRecentQuery: @escaping (String) -> Void, toggleSelection: @escaping ([String], Bool) -> Void, sendSelected: @escaping (ChatContextResultCollection) -> Void) {
init(openResult: @escaping (ChatContextResult) -> Void, setSearchQuery: @escaping (String) -> Void, deleteRecentQuery: @escaping (String) -> Void, toggleSelection: @escaping ([String], Bool) -> Void, sendSelected: @escaping (ChatContextResultCollection, ChatContextResult?) -> Void, editingContext: TGMediaEditingContext) {
self.openResult = openResult
self.setSearchQuery = setSearchQuery
self.deleteRecentQuery = deleteRecentQuery
self.toggleSelection = toggleSelection
self.sendSelected = sendSelected
self.editingContext = editingContext
}
}
@ -69,7 +73,7 @@ final class WebSearchController: ViewController {
private var navigationContentNode: WebSearchNavigationContentNode?
init(account: Account, chatLocation: ChatLocation, configuration: SearchBotsConfiguration, sendSelected: @escaping ([String], ChatContextResultCollection) -> Void) {
init(account: Account, chatLocation: ChatLocation, configuration: SearchBotsConfiguration, sendSelected: @escaping ([String], ChatContextResultCollection, TGMediaEditingContext) -> Void) {
self.account = account
self.chatLocation = chatLocation
self.configuration = configuration
@ -117,8 +121,15 @@ final class WebSearchController: ViewController {
}
self.navigationBar?.setContentNode(navigationContentNode, animated: false)
self.controllerInteraction = WebSearchControllerInteraction(openResult: { result in
let editingContext = TGMediaEditingContext()
self.controllerInteraction = WebSearchControllerInteraction(openResult: { [weak self] result in
if let strongSelf = self {
strongSelf.controllerNode.openResult(currentResult: result, present: { [weak self] viewController, arguments in
if let strongSelf = self {
strongSelf.present(viewController, in: .window(.root), with: arguments)
}
})
}
}, setSearchQuery: { [weak self] query in
if let strongSelf = self {
strongSelf.navigationContentNode?.setQuery(query)
@ -133,11 +144,15 @@ final class WebSearchController: ViewController {
if let strongSelf = self {
strongSelf.updateInterfaceState { $0.withToggledSelectedMessages(ids, value: value) }
}
}, sendSelected: { [weak self] collection in
if let strongSelf = self, let state = strongSelf.interfaceState.state, !state.selectionState.selectedIds.isEmpty {
sendSelected(Array(state.selectionState.selectedIds), collection)
}, sendSelected: { [weak self] collection, current in
if let strongSelf = self, let state = strongSelf.interfaceState.state {
var selectedIds = state.selectionState.selectedIds
if let current = current {
selectedIds.insert(current.id)
}
sendSelected(Array(selectedIds), collection, editingContext)
}
})
}, editingContext: editingContext)
}
required public init(coder aDecoder: NSCoder) {

View File

@ -4,6 +4,7 @@ import Postbox
import SwiftSignalKit
import Display
import TelegramCore
import LegacyComponents
private struct WebSearchContextResultStableId: Hashable {
let result: ChatContextResult
@ -144,12 +145,17 @@ class WebSearchControllerNode: ASDisplayNode {
private var hasMore = false
private var isLoadingMore = false
private let selectionContext = TGMediaSelectionContext()
private let hiddenMediaId = Promise<String?>(nil)
private var hiddenMediaDisposable: Disposable?
private let results = ValuePromise<ChatContextResultCollection?>(nil, ignoreRepeated: true)
private let disposable = MetaDisposable()
private let loadMoreDisposable = MetaDisposable()
private let recentDisposable = MetaDisposable()
private var recentDisposable: Disposable?
private var containerLayout: (ContainerViewLayout, CGFloat)?
@ -219,7 +225,7 @@ class WebSearchControllerNode: ASDisplayNode {
}))
let previousRecentItems = Atomic<[WebSearchRecentQueryEntry]?>(value: nil)
self.recentDisposable.set((combineLatest(webSearchRecentQueries(postbox: self.account.postbox), self.webSearchInterfaceStatePromise.get())
self.recentDisposable = (combineLatest(webSearchRecentQueries(postbox: self.account.postbox), self.webSearchInterfaceStatePromise.get())
|> deliverOnMainQueue).start(next: { [weak self] queries, interfaceState in
if let strongSelf = self {
var entries: [WebSearchRecentQueryEntry] = []
@ -236,7 +242,7 @@ class WebSearchControllerNode: ASDisplayNode {
let transition = preparedWebSearchRecentTransition(from: previousEntries ?? [], to: entries, account: strongSelf.account, theme: interfaceState.presentationData.theme, strings: interfaceState.presentationData.strings, controllerInteraction: strongSelf.controllerInteraction, header: header)
strongSelf.enqueueRecentTransition(transition, firstTime: previousEntries == nil)
}
}))
})
self.gridNode.visibleItemsUpdated = { [weak self] visibleItems in
if let strongSelf = self, let bottom = visibleItems.bottom, let entries = strongSelf.currentEntries {
@ -253,10 +259,26 @@ class WebSearchControllerNode: ASDisplayNode {
self.recentQueriesNode.beganInteractiveDragging = { [weak self] in
self?.dismissInput?()
}
self.hiddenMediaDisposable = (self.hiddenMediaId.get()
|> deliverOnMainQueue).start(next: { [weak self] id in
if let strongSelf = self {
strongSelf.controllerInteraction.hiddenMediaId = id
strongSelf.gridNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? WebSearchItemNode {
itemNode.updateHiddenMedia()
}
}
}
})
}
deinit {
self.disposable.dispose()
self.recentDisposable?.dispose()
self.loadMoreDisposable.dispose()
self.hiddenMediaDisposable?.dispose()
}
func updatePresentationData(theme: PresentationTheme, strings: PresentationStrings) {
@ -519,8 +541,79 @@ class WebSearchControllerNode: ASDisplayNode {
@objc private func sendPressed() {
if let results = self.currentProcessedResults {
self.controllerInteraction.sendSelected(results)
self.controllerInteraction.sendSelected(results, nil)
}
self.cancel?()
}
func openResult(currentResult: ChatContextResult, present: (ViewController, Any?) -> Void) {
if let state = self.webSearchInterfaceState.state, state.mode == .images {
if let results = self.currentProcessedResults?.results {
presentLegacyWebSearchGallery(account: self.account, peer: nil, theme: self.theme, results: results, current: currentResult, selectionContext: self.selectionContext, editingContext: self.controllerInteraction.editingContext, updateHiddenMedia: { [weak self] id in
self?.hiddenMediaId.set(.single(id))
}, transitionHostView: { [weak self] in
return self?.gridNode.view
}, transitionView: { [weak self] result in
return self?.transitionView(for: result)
}, completed: { [weak self] result in
if let strongSelf = self, let results = strongSelf.currentProcessedResults {
strongSelf.controllerInteraction.sendSelected(results, nil)
strongSelf.cancel?()
}
}, present: present)
}
} else {
if let results = self.currentProcessedResults?.results {
var entries: [WebSearchGalleryEntry] = []
var centralIndex: Int = 0
for i in 0 ..< results.count {
entries.append(WebSearchGalleryEntry(result: results[i]))
if results[i] == currentResult {
centralIndex = i
}
}
let controller = WebSearchGalleryController(account: self.account, entries: entries, centralIndex: centralIndex, replaceRootController: { (controller, _) in
}, baseNavigationController: nil)
self.hiddenMediaId.set((controller.hiddenMedia |> deliverOnMainQueue)
|> map { entry in
return entry?.result.id
})
present(controller, WebSearchGalleryControllerPresentationArguments(transitionArguments: { [weak self] entry -> GalleryTransitionArguments? in
if let strongSelf = self {
var transitionNode: WebSearchItemNode?
strongSelf.gridNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? WebSearchItemNode, itemNode.item?.result.id == entry.result.id {
transitionNode = itemNode
}
}
if let transitionNode = transitionNode {
return GalleryTransitionArguments(transitionNode: (transitionNode, { [weak transitionNode] in
return transitionNode?.transitionView().snapshotContentTree(unhide: true)
}), addToTransitionSurface: { view in
if let strongSelf = self {
strongSelf.gridNode.view.superview?.insertSubview(view, aboveSubview: strongSelf.gridNode.view)
}
})
}
}
return nil
}))
}
}
}
private func transitionView(for result: ChatContextResult) -> UIView? {
var transitionNode: WebSearchItemNode?
self.gridNode.forEachItemNode { itemNode in
if let itemNode = itemNode as? WebSearchItemNode, itemNode.item?.result.id == result.id {
transitionNode = itemNode
}
}
if let transitionNode = transitionNode {
return transitionNode.transitionView()
}
return nil
}
}

View File

@ -0,0 +1,254 @@
import Foundation
import Display
import QuickLook
import Postbox
import SwiftSignalKit
import AsyncDisplayKit
import TelegramCore
import SafariServices
struct WebSearchGalleryEntry: Equatable {
let result: ChatContextResult
static func ==(lhs: WebSearchGalleryEntry, rhs: WebSearchGalleryEntry) -> Bool {
return lhs.result == rhs.result
}
func item(account: Account, presentationData: PresentationData) -> GalleryItem {
switch self.result {
case let .externalReference(queryId, id, type, _, _, url, content, thumbnail, _):
if let content = content, type == "gif", let thumbnailResource = thumbnail?.resource, let dimensions = content.dimensions {
let fileReference = FileMediaReference.standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: content.resource, previewRepresentations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnailResource)], mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])]))
return UniversalVideoGalleryItem(account: account, presentationData: presentationData, content: NativeVideoContent(id: .contextResult(self.result.queryId, self.result.id), fileReference: fileReference, streamVideo: false, loopVideo: true, enableSound: false, fetchAutomatically: true), originData: nil, indexData: nil, contentInfo: nil, caption: NSAttributedString(), credit: nil, performAction: { _ in }, openActionOptions: { _ in })
}
case let .internalReference(queryId, id, _, _, _, _, file, _):
if let file = file {
return UniversalVideoGalleryItem(account: account, presentationData: presentationData, content: NativeVideoContent(id: .contextResult(self.result.queryId, self.result.id), fileReference: .standalone(media: file), streamVideo: false, loopVideo: true, enableSound: false, fetchAutomatically: true), originData: nil, indexData: nil, contentInfo: nil, caption: NSAttributedString(), credit: nil, performAction: { _ in }, openActionOptions: { _ in })
}
}
preconditionFailure()
}
}
final class WebSearchGalleryControllerPresentationArguments {
let transitionArguments: (WebSearchGalleryEntry) -> GalleryTransitionArguments?
init(transitionArguments: @escaping (WebSearchGalleryEntry) -> GalleryTransitionArguments?) {
self.transitionArguments = transitionArguments
}
}
class WebSearchGalleryController: ViewController {
private var galleryNode: GalleryControllerNode {
return self.displayNode as! GalleryControllerNode
}
private let account: Account
private var presentationData: PresentationData
private let _ready = Promise<Bool>()
override var ready: Promise<Bool> {
return self._ready
}
private var didSetReady = false
private let disposable = MetaDisposable()
private var entries: [WebSearchGalleryEntry] = []
private var centralEntryIndex: Int?
private let centralItemTitle = Promise<String>()
private let centralItemTitleView = Promise<UIView?>()
private let centralItemNavigationStyle = Promise<GalleryItemNodeNavigationStyle>()
private let centralItemFooterContentNode = Promise<GalleryFooterContentNode?>()
private let centralItemAttributesDisposable = DisposableSet();
private let _hiddenMedia = Promise<WebSearchGalleryEntry?>(nil)
var hiddenMedia: Signal<WebSearchGalleryEntry?, NoError> {
return self._hiddenMedia.get()
}
private let replaceRootController: (ViewController, ValuePromise<Bool>?) -> Void
private let baseNavigationController: NavigationController?
init(account: Account, entries: [WebSearchGalleryEntry], centralIndex: Int, replaceRootController: @escaping (ViewController, ValuePromise<Bool>?) -> Void, baseNavigationController: NavigationController?) {
self.account = account
self.replaceRootController = replaceRootController
self.baseNavigationController = baseNavigationController
self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: GalleryController.darkNavigationTheme, strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)))
let backItem = UIBarButtonItem(backButtonAppearanceWithTitle: presentationData.strings.Common_Back, target: self, action: #selector(self.donePressed))
self.navigationItem.leftBarButtonItem = backItem
self.statusBar.statusBarStyle = .White
let entriesSignal: Signal<[WebSearchGalleryEntry], NoError> = .single(entries)
self.disposable.set((entriesSignal |> deliverOnMainQueue).start(next: { [weak self] entries in
if let strongSelf = self {
strongSelf.entries = entries
strongSelf.centralEntryIndex = centralIndex
if strongSelf.isViewLoaded {
strongSelf.galleryNode.pager.replaceItems(strongSelf.entries.map({
$0.item(account: account, presentationData: strongSelf.presentationData)
}), centralItemIndex: centralIndex, keepFirst: false)
let ready = strongSelf.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak strongSelf] _ in
strongSelf?.didSetReady = true
}
strongSelf._ready.set(ready |> map { true })
}
}
}))
self.centralItemAttributesDisposable.add(self.centralItemTitle.get().start(next: { [weak self] title in
self?.navigationItem.title = title
}))
self.centralItemAttributesDisposable.add(self.centralItemTitleView.get().start(next: { [weak self] titleView in
self?.navigationItem.titleView = titleView
}))
self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode in
self?.galleryNode.updatePresentationState({
$0.withUpdatedFooterContentNode(footerContentNode)
}, transition: .immediate)
}))
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.disposable.dispose()
self.centralItemAttributesDisposable.dispose()
}
@objc func donePressed() {
self.dismiss(forceAway: false)
}
private func dismiss(forceAway: Bool) {
var animatedOutNode = true
var animatedOutInterface = false
let completion = { [weak self] in
if animatedOutNode && animatedOutInterface {
self?._hiddenMedia.set(.single(nil))
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}
}
if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? WebSearchGalleryControllerPresentationArguments {
if !self.entries.isEmpty {
if let transitionArguments = presentationArguments.transitionArguments(self.entries[centralItemNode.index]), !forceAway {
animatedOutNode = false
centralItemNode.animateOut(to: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface, completion: {
animatedOutNode = true
completion()
})
}
}
}
self.galleryNode.animateOut(animateContent: animatedOutNode, completion: {
animatedOutInterface = true
completion()
})
}
override func loadDisplayNode() {
let controllerInteraction = GalleryControllerInteraction(presentController: { [weak self] controller, arguments in
if let strongSelf = self {
strongSelf.present(controller, in: .window(.root), with: arguments)
}
}, dismissController: { [weak self] in
self?.dismiss(forceAway: true)
}, replaceRootController: { [weak self] controller, ready in
if let strongSelf = self {
strongSelf.replaceRootController(controller, ready)
}
})
self.displayNode = GalleryControllerNode(controllerInteraction: controllerInteraction)
self.displayNodeDidLoad()
self.galleryNode.statusBar = self.statusBar
self.galleryNode.navigationBar = self.navigationBar
self.galleryNode.transitionDataForCentralItem = { [weak self] in
if let strongSelf = self {
if let centralItemNode = strongSelf.galleryNode.pager.centralItemNode(), let presentationArguments = strongSelf.presentationArguments as? WebSearchGalleryControllerPresentationArguments {
if let transitionArguments = presentationArguments.transitionArguments(strongSelf.entries[centralItemNode.index]) {
return (transitionArguments.transitionNode, transitionArguments.addToTransitionSurface)
}
}
}
return nil
}
self.galleryNode.dismiss = { [weak self] in
self?._hiddenMedia.set(.single(nil))
self?.presentingViewController?.dismiss(animated: false, completion: nil)
}
self.galleryNode.pager.replaceItems(self.entries.map({
$0.item(account: account, presentationData: self.presentationData)
}), centralItemIndex: self.centralEntryIndex)
self.galleryNode.pager.centralItemIndexUpdated = { [weak self] index in
if let strongSelf = self {
var hiddenItem: WebSearchGalleryEntry?
if let index = index {
hiddenItem = strongSelf.entries[index]
if let node = strongSelf.galleryNode.pager.centralItemNode() {
strongSelf.centralItemTitle.set(node.title())
strongSelf.centralItemTitleView.set(node.titleView())
strongSelf.centralItemNavigationStyle.set(node.navigationStyle())
strongSelf.centralItemFooterContentNode.set(node.footerContent())
}
}
if strongSelf.didSetReady {
strongSelf._hiddenMedia.set(.single(hiddenItem))
}
}
}
let ready = self.galleryNode.pager.ready() |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(Void())) |> afterNext { [weak self] _ in
self?.didSetReady = true
}
self._ready.set(ready |> map { true })
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
var nodeAnimatesItself = false
if let centralItemNode = self.galleryNode.pager.centralItemNode(), let presentationArguments = self.presentationArguments as? WebSearchGalleryControllerPresentationArguments {
self.centralItemTitle.set(centralItemNode.title())
self.centralItemTitleView.set(centralItemNode.titleView())
self.centralItemNavigationStyle.set(centralItemNode.navigationStyle())
self.centralItemFooterContentNode.set(centralItemNode.footerContent())
if let transitionArguments = presentationArguments.transitionArguments(self.entries[centralItemNode.index]) {
nodeAnimatesItself = true
centralItemNode.animateIn(from: transitionArguments.transitionNode, addToTransitionSurface: transitionArguments.addToTransitionSurface)
self._hiddenMedia.set(.single(self.entries[centralItemNode.index]))
}
}
self.galleryNode.animateIn(animateContent: !nodeAnimatesItself)
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.galleryNode.frame = CGRect(origin: CGPoint(), size: layout.size)
self.galleryNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition)
}
}

View File

@ -89,7 +89,6 @@ final class WebSearchItemNode: GridItemNode {
var updatedStatusSignal: Signal<MediaResourceStatus, NoError>?
var imageResource: TelegramMediaResource?
var stickerFile: TelegramMediaFile?
var videoFile: TelegramMediaFile?
var imageDimensions: CGSize?
switch item.result {
@ -147,7 +146,57 @@ final class WebSearchItemNode: GridItemNode {
}
if let updateImageSignal = updateImageSignal {
self.imageNode.setSignal(updateImageSignal)
let editingContext = item.controllerInteraction.editingContext
let editedImageSignal = Signal<UIImage?, NoError> { subscriber in
let editableItem = LegacyWebSearchItem(result: item.result, dimensions: CGSize(), thumbnailImage: .complete(), originalImage: .complete())
if let signal = editingContext.thumbnailImageSignal(for: editableItem) {
let disposable = signal.start(next: { next in
if let image = next as? UIImage {
subscriber.putNext(image)
} else {
subscriber.putNext(nil)
}
}, error: { _ in
}, completed: nil)!
return ActionDisposable {
disposable.dispose()
}
} else {
return EmptyDisposable
}
}
let editedSignal: Signal<((TransformImageArguments) -> DrawingContext?)?, NoError> = editedImageSignal
|> map { image in
if let image = image {
return { arguments in
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)
context.withFlippedContext { c in
c.setBlendMode(.copy)
if let cgImage = image.cgImage {
drawImage(context: c, image: cgImage, orientation: .up, in: fittedRect)
}
}
return context
}
} else {
return nil
}
}
let imageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError> = editedSignal
|> mapToSignal { result in
if result != nil {
return .single(result!)
} else {
return updateImageSignal
}
}
self.imageNode.setSignal(imageSignal)
}
self.currentImageResource = imageResource
@ -170,10 +219,9 @@ final class WebSearchItemNode: GridItemNode {
strongSelf.updateSelectionState(animated: true)
}
})
selectionNode.frame = CGRect(origin: CGPoint(), size: self.bounds.size)
self.addSubnode(selectionNode)
self.selectionNode = selectionNode
self.setNeedsLayout()
}
if let item = self.item {
@ -184,6 +232,18 @@ final class WebSearchItemNode: GridItemNode {
}
}
func updateHiddenMedia() {
if let item = self.item {
self.imageNode.isHidden = item.controllerInteraction.hiddenMediaId == item.result.id
}
}
func transitionView() -> UIView {
let view = self.imageNode.view.snapshotContentTree(unhide: true)!
view.frame = self.convert(self.bounds, to: nil)
return view
}
override func layout() {
super.layout()
@ -195,7 +255,8 @@ final class WebSearchItemNode: GridItemNode {
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageFrame.size, intrinsicInsets: UIEdgeInsets(), emptyColor: item.theme.list.mediaPlaceholderColor))()
}
self.selectionNode?.frame = CGRect(origin: CGPoint(), size: self.bounds.size)
let checkSize = CGSize(width: 32.0, height: 32.0)
self.selectionNode?.frame = CGRect(origin: CGPoint(x: imageFrame.width - checkSize.width, y: 0.0), size: checkSize)
let progressDiameter: CGFloat = 40.0
self.statusNode.frame = CGRect(origin: CGPoint(x: floor((imageFrame.size.width - progressDiameter) / 2.0), y: floor((imageFrame.size.height - progressDiameter) / 2.0)), size: CGSize(width: progressDiameter, height: progressDiameter))
@ -203,40 +264,26 @@ final class WebSearchItemNode: GridItemNode {
}
@objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
// guard let controllerInteraction = self.controllerInteraction, let message = self.item?.message else {
// return
// }
//
// switch recognizer.state {
// case .ended:
// if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation {
// switch gesture {
// case .tap:
// if let (account, media, _) = self.currentState {
// if let file = media as? TelegramMediaFile {
// if let resourceStatus = self.resourceStatus {
// switch resourceStatus {
// case .Fetching:
// messageMediaFileCancelInteractiveFetch(account: account, messageId: message.id, file: file)
// case .Local:
// let _ = controllerInteraction.openMessage(message, .default)
// case .Remote:
// self.fetchDisposable.set(messageMediaFileInteractiveFetched(account: account, message: message, file: file, userInitiated: true).start())
// }
// }
// } else {
// let _ = controllerInteraction.openMessage(message, .default)
// }
// }
// case .longTap:
// controllerInteraction.openMessageContextMenu(message, false, self, self.bounds)
// default:
// break
// }
// }
// default:
// break
// }
guard let item = self.item else {
return
}
switch recognizer.state {
case .ended:
if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
item.controllerInteraction.openResult(item.result)
case .longTap:
break
default:
break
}
}
default:
break
}
}
}