Various improvements

This commit is contained in:
Ilya Laktyushin 2020-11-04 21:12:12 +04:00
parent bc793ac286
commit f306171529
53 changed files with 5608 additions and 4579 deletions

View File

@ -5868,6 +5868,7 @@ Sorry for the inconvenience.";
"Chat.PinnedMessagesHiddenText" = "You will see the bar with pinned messages only if a new message is pinned.";
"OpenFile.PotentiallyDangerousContentAlert" = "Previewing this file can potentially expose your IP address to its sender. Continue?";
"OpenFile.Proceed" = "Proceed";
"Chat.PinnedListPreview.ShowAllMessages" = "Show All Messages";
"Chat.PinnedListPreview.UnpinAllMessages" = "Unpin All Messages";
@ -5890,3 +5891,10 @@ Sorry for the inconvenience.";
"Stats.Message.PrivateShares" = "Private Shares";
"ChatSettings.WidgetSettings" = "Widget";
"Conversation.AddCaption" = "Add a Caption";
"Conversation.EditCaption" = "Edit Caption";
"Conversation.EditThisPhoto" = "Edit This Photo";
"Conversation.ReplacePhoto" = "Replace Photo";
"Conversation.ReplaceFile" = "Replace File";
"Conversation.SendAsNewPhoto" = "Send as New Photo";

View File

@ -18,8 +18,9 @@ public final class GalleryControllerActionInteraction {
public let openBotCommand: (String) -> Void
public let addContact: (String) -> Void
public let storeMediaPlaybackState: (MessageId, Double?) -> Void
public let editMedia: (MessageId) -> Void
public init(openUrl: @escaping (String, Bool) -> Void, openUrlIn: @escaping (String) -> Void, openPeerMention: @escaping (String) -> Void, openPeer: @escaping (PeerId) -> Void, openHashtag: @escaping (String?, String) -> Void, openBotCommand: @escaping (String) -> Void, addContact: @escaping (String) -> Void, storeMediaPlaybackState: @escaping (MessageId, Double?) -> Void) {
public init(openUrl: @escaping (String, Bool) -> Void, openUrlIn: @escaping (String) -> Void, openPeerMention: @escaping (String) -> Void, openPeer: @escaping (PeerId) -> Void, openHashtag: @escaping (String?, String) -> Void, openBotCommand: @escaping (String) -> Void, addContact: @escaping (String) -> Void, storeMediaPlaybackState: @escaping (MessageId, Double?) -> Void, editMedia: @escaping (MessageId) -> Void) {
self.openUrl = openUrl
self.openUrlIn = openUrlIn
self.openPeerMention = openPeerMention
@ -28,5 +29,6 @@ public final class GalleryControllerActionInteraction {
self.openBotCommand = openBotCommand
self.addContact = addContact
self.storeMediaPlaybackState = storeMediaPlaybackState
self.editMedia = editMedia
}
}

View File

@ -264,6 +264,7 @@ private final class ChatListContainerItemNode: ASDisplayNode {
private(set) var emptyNode: ChatListEmptyNode?
var emptyShimmerEffectNode: ChatListShimmerNode?
private var shimmerNodeOffset: CGFloat = 0.0
let listNode: ChatListNode
private var validLayout: (CGSize, UIEdgeInsets, CGFloat)?
@ -285,8 +286,12 @@ private final class ChatListContainerItemNode: ASDisplayNode {
return
}
var needsShimmerNode = false
var shimmerNodeOffset: CGFloat = 0.0
switch isEmptyState {
case let .empty(isLoading):
case let .empty(isLoading, hasArchiveInfo):
if hasArchiveInfo {
shimmerNodeOffset = 253.0
}
if isLoading {
needsShimmerNode = true
@ -326,12 +331,13 @@ private final class ChatListContainerItemNode: ASDisplayNode {
}
}
if needsShimmerNode {
strongSelf.shimmerNodeOffset = shimmerNodeOffset
if strongSelf.emptyShimmerEffectNode == nil {
let emptyShimmerEffectNode = ChatListShimmerNode()
strongSelf.emptyShimmerEffectNode = emptyShimmerEffectNode
strongSelf.insertSubnode(emptyShimmerEffectNode, belowSubnode: strongSelf.listNode)
if let (size, insets, _) = strongSelf.validLayout, let offset = strongSelf.floatingHeaderOffset {
strongSelf.layoutEmptyShimmerEffectNode(node: emptyShimmerEffectNode, size: size, insets: insets, verticalOffset: offset, transition: .immediate)
strongSelf.layoutEmptyShimmerEffectNode(node: emptyShimmerEffectNode, size: size, insets: insets, verticalOffset: offset + strongSelf.shimmerNodeOffset, transition: .immediate)
}
}
} else if let emptyShimmerEffectNode = strongSelf.emptyShimmerEffectNode {
@ -349,7 +355,7 @@ private final class ChatListContainerItemNode: ASDisplayNode {
}
strongSelf.floatingHeaderOffset = offset
if let (size, insets, _) = strongSelf.validLayout, let emptyShimmerEffectNode = strongSelf.emptyShimmerEffectNode {
strongSelf.layoutEmptyShimmerEffectNode(node: emptyShimmerEffectNode, size: size, insets: insets, verticalOffset: offset, transition: transition)
strongSelf.layoutEmptyShimmerEffectNode(node: emptyShimmerEffectNode, size: size, insets: insets, verticalOffset: offset + strongSelf.shimmerNodeOffset, transition: transition)
}
}
}

View File

@ -403,7 +403,7 @@ public enum ChatListNodeScrollPosition {
public enum ChatListNodeEmptyState: Equatable {
case notEmpty(containsChats: Bool)
case empty(isLoading: Bool)
case empty(isLoading: Bool, hasArchiveInfo: Bool)
}
public final class ChatListNode: ListView {
@ -1411,6 +1411,7 @@ public final class ChatListNode: ListView {
var isEmpty = false
var isLoading = false
var hasArchiveInfo = false
if transition.chatListView.filteredEntries.isEmpty {
isEmpty = true
} else {
@ -1421,6 +1422,9 @@ public final class ChatListNode: ListView {
case .GroupReferenceEntry, .HeaderEntry, .HoleEntry:
break
default:
if case .ArchiveIntro = entry {
hasArchiveInfo = true
}
isEmpty = false
break loop1
}
@ -1441,14 +1445,21 @@ public final class ChatListNode: ListView {
if !hasHoles {
isLoading = false
}
} else {
for entry in transition.chatListView.filteredEntries.reversed().prefix(2) {
if case .ArchiveIntro = entry {
hasArchiveInfo = true
break
}
}
}
}
let isEmptyState: ChatListNodeEmptyState
if transition.chatListView.isLoading {
isEmptyState = .empty(isLoading: true)
isEmptyState = .empty(isLoading: true, hasArchiveInfo: hasArchiveInfo)
} else if isEmpty {
isEmptyState = .empty(isLoading: isLoading)
isEmptyState = .empty(isLoading: isLoading, hasArchiveInfo: false)
} else {
var containsChats = false
loop: for entry in transition.chatListView.filteredEntries {

View File

@ -4,7 +4,7 @@
bool TGIsGzippedData(NSData *data) {
const UInt8 *bytes = (const UInt8 *)data.bytes;
return (data.length >= 2 && bytes[0] == 0x1f && bytes[1] == 0x8b);
return data.length >= 2 && ((bytes[0] == 0x1f && bytes[1] == 0x8b) || (bytes[0] == 0x78 && bytes[1] == 0x9c));
}
NSData *TGGZipData(NSData *data, float level) {

View File

@ -21,6 +21,7 @@ import TextSelectionNode
private let deleteImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: .white)
private let actionImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: .white)
private let editImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/Draw"), color: .white)
private let backwardImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/BackwardButton"), color: .white)
private let forwardImage = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/ForwardButton"), color: .white)
@ -47,7 +48,7 @@ private let dateFont = Font.regular(14.0)
enum ChatItemGalleryFooterContent: Equatable {
case info
case fetch(status: MediaResourceStatus)
case fetch(status: MediaResourceStatus, seekable: Bool)
case playback(paused: Bool, seekable: Bool)
static func ==(lhs: ChatItemGalleryFooterContent, rhs: ChatItemGalleryFooterContent) -> Bool {
@ -58,8 +59,8 @@ enum ChatItemGalleryFooterContent: Equatable {
} else {
return false
}
case let .fetch(lhsStatus):
if case let .fetch(rhsStatus) = rhs, lhsStatus == rhsStatus {
case let .fetch(lhsStatus, lhsSeekable):
if case let .fetch(rhsStatus, rhsSeekable) = rhs, lhsStatus == rhsStatus, lhsSeekable == rhsSeekable {
return true
} else {
return false
@ -116,6 +117,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
private let contentNode: ASDisplayNode
private let deleteButton: UIButton
private let actionButton: UIButton
private let editButton: UIButton
private let maskNode: ASDisplayNode
private let scrollWrapperNode: CaptionScrollWrapperNode
private let scrollNode: ASScrollNode
@ -165,11 +167,11 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.playbackControlButton.isHidden = true
self.statusButtonNode.isHidden = true
self.statusNode.isHidden = true
case let .fetch(status):
case let .fetch(status, seekable):
self.authorNameNode.isHidden = true
self.dateNode.isHidden = true
self.backwardButton.isHidden = true
self.forwardButton.isHidden = true
self.backwardButton.isHidden = !seekable
self.forwardButton.isHidden = !seekable
if status == .Local {
self.playbackControlButton.isHidden = false
self.playbackControlButton.setImage(playImage, for: [])
@ -217,8 +219,8 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
}
}
didSet {
if let scrubberView = self.scrubberView {
scrubberView.setCollapsed(self.visibilityAlpha < 1.0 ? true : false, animated: false)
if let scrubberView = self.scrubberView {
scrubberView.setCollapsed(self.visibilityAlpha < 1.0, animated: false)
self.view.addSubview(scrubberView)
scrubberView.updateScrubbingVisual = { [weak self] value in
guard let strongSelf = self else {
@ -253,7 +255,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
override func setVisibilityAlpha(_ alpha: CGFloat, animated: Bool) {
self.visibilityAlpha = alpha
self.contentNode.alpha = alpha
self.scrubberView?.setCollapsed(alpha < 1.0 ? true : false, animated: animated)
self.scrubberView?.setCollapsed(alpha < 1.0, animated: animated)
}
init(context: AccountContext, presentationData: PresentationData, present: @escaping (ViewController, Any?) -> Void = { _, _ in }) {
@ -268,9 +270,11 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.deleteButton = UIButton()
self.actionButton = UIButton()
self.editButton = UIButton()
self.deleteButton.setImage(deleteImage, for: [.normal])
self.actionButton.setImage(actionImage, for: [.normal])
self.editButton.setImage(editImage, for: [.normal])
self.scrollWrapperNode = CaptionScrollWrapperNode()
self.scrollWrapperNode.clipsToBounds = true
@ -340,6 +344,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.contentNode.view.addSubview(self.deleteButton)
self.contentNode.view.addSubview(self.actionButton)
self.contentNode.view.addSubview(self.editButton)
self.contentNode.addSubnode(self.scrollWrapperNode)
self.scrollWrapperNode.addSubnode(self.scrollNode)
self.scrollNode.addSubnode(self.textNode)
@ -356,6 +361,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.deleteButton.addTarget(self, action: #selector(self.deleteButtonPressed), for: [.touchUpInside])
self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), for: [.touchUpInside])
self.editButton.addTarget(self, action: #selector(self.editButtonPressed), for: [.touchUpInside])
self.backwardButton.addTarget(self, action: #selector(self.backwardButtonPressed), forControlEvents: .touchUpInside)
self.forwardButton.addTarget(self, action: #selector(self.forwardButtonPressed), forControlEvents: .touchUpInside)
@ -444,6 +450,16 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
let canDelete: Bool
var canShare = !message.containsSecretMedia && !Namespaces.Message.allScheduled.contains(message.id.namespace)
var canEdit = false
for media in message.media {
if media is TelegramMediaImage {
canEdit = true
break
}
}
canEdit = canEdit && !message.containsSecretMedia
if let peer = message.peers[message.id.peerId] {
if peer is TelegramUser || peer is TelegramSecretChat {
canDelete = true
@ -452,17 +468,21 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
} else if let channel = peer as? TelegramChannel {
if message.flags.contains(.Incoming) {
canDelete = channel.hasPermission(.deleteAllMessages)
canEdit = canEdit && channel.hasPermission(.editAllMessages)
} else {
canDelete = true
}
} else {
canDelete = false
canEdit = false
}
} else {
canDelete = false
canShare = false
canEdit = false
}
var authorNameText: String?
if let author = message.effectiveAuthor {
authorNameText = author.displayTitle(strings: self.strings, displayOrder: self.nameOrder)
@ -496,10 +516,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
}
messageText = galleryCaptionStringWithAppliedEntities(message.text, entities: entities)
}
self.editButton.isHidden = message.containsSecretMedia
self.actionButton.isHidden = message.containsSecretMedia || Namespaces.Message.allScheduled.contains(message.id.namespace)
if self.currentMessageText != messageText || canDelete != !self.deleteButton.isHidden || canShare != !self.actionButton.isHidden || self.currentAuthorNameText != authorNameText || self.currentDateText != dateText {
if self.currentMessageText != messageText || canDelete != !self.deleteButton.isHidden || canShare != !self.actionButton.isHidden || canEdit != !self.editButton.isHidden || self.currentAuthorNameText != authorNameText || self.currentDateText != dateText {
self.currentMessageText = messageText
if messageText.length == 0 {
@ -519,6 +539,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.deleteButton.isHidden = !canDelete
self.actionButton.isHidden = !canShare
self.editButton.isHidden = !canEdit
self.requestLayout?(.immediate)
}
@ -620,20 +641,22 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
if self.textNode.isHidden || !displayCaption {
panelHeight += 8.0
} else {
scrubberY = panelHeight - bottomInset - 44.0 - 41.0
scrubberY = panelHeight - bottomInset - 44.0 - 44.0
if contentInset > 0.0 {
scrubberY -= contentInset + 3.0
scrubberY -= contentInset
}
}
let scrubberFrame = CGRect(origin: CGPoint(x: leftInset, y: scrubberY), size: CGSize(width: width - leftInset - rightInset, height: 34.0))
scrubberView.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate)
transition.updateFrame(layer: scrubberView.layer, frame: scrubberFrame)
transition.updateBounds(layer: scrubberView.layer, bounds: CGRect(origin: CGPoint(), size: scrubberFrame.size))
transition.updatePosition(layer: scrubberView.layer, position: CGPoint(x: scrubberFrame.midX, y: scrubberFrame.midY))
}
transition.updateAlpha(node: self.textNode, alpha: displayCaption ? 1.0 : 0.0)
self.actionButton.frame = CGRect(origin: CGPoint(x: leftInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0))
self.deleteButton.frame = CGRect(origin: CGPoint(x: width - 44.0 - rightInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0))
self.editButton.frame = CGRect(origin: CGPoint(x: width - 44.0 - 50.0 - rightInset, y: panelHeight - bottomInset - 44.0), size: CGSize(width: 44.0, height: 44.0))
if let image = self.backwardButton.image(for: .normal) {
self.backwardButton.frame = CGRect(origin: CGPoint(x: floor((width - image.size.width) / 2.0) - 66.0, y: panelHeight - bottomInset - 44.0 + 7.0), size: image.size)
@ -708,6 +731,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.authorNameNode.alpha = 1.0
self.deleteButton.alpha = 1.0
self.actionButton.alpha = 1.0
self.editButton.alpha = 1.0
self.backwardButton.alpha = 1.0
self.forwardButton.alpha = 1.0
self.statusNode.alpha = 1.0
@ -730,6 +754,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
self.authorNameNode.alpha = 0.0
self.deleteButton.alpha = 0.0
self.actionButton.alpha = 0.0
self.editButton.alpha = 0.0
self.backwardButton.alpha = 0.0
self.forwardButton.alpha = 0.0
self.statusNode.alpha = 0.0
@ -1049,6 +1074,13 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
}
}
@objc func editButtonPressed() {
guard let message = self.currentMessage else {
return
}
self.controllerInteraction?.editMedia(message.id)
}
@objc func playbackControlPressed() {
self.playbackControl?()
}

View File

@ -441,9 +441,9 @@ public class GalleryController: ViewController, StandalonePresentableController
namespaces = .not(Namespaces.Message.allScheduled)
}
return context.account.postbox.aroundMessageHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), anchor: .index(message!.index), count: 50, clipHoles: false, fixedCombinedReadStates: nil, topTaggedMessageIdNamespaces: [], tagMask: tags, appendMessagesFromTheSameGroup: false, namespaces: namespaces, orderStatistics: [.combinedLocation])
|> mapToSignal { (view, _, _) -> Signal<GalleryMessageHistoryView?, NoError> in
let mapped = GalleryMessageHistoryView.view(view)
return .single(mapped)
|> mapToSignal { (view, _, _) -> Signal<GalleryMessageHistoryView?, NoError> in
let mapped = GalleryMessageHistoryView.view(view)
return .single(mapped)
}
} else {
return .single(GalleryMessageHistoryView.entries([MessageHistoryEntry(message: message!, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false))], false, false))
@ -911,6 +911,11 @@ public class GalleryController: ViewController, StandalonePresentableController
if let strongSelf = self {
strongSelf.replaceRootController(controller, ready)
}
}, editMedia: { [weak self] messageId in
if let strongSelf = self {
strongSelf.dismiss(forceAway: true)
strongSelf.actionInteraction?.editMedia(messageId)
}
})
self.displayNode = GalleryControllerNode(controllerInteraction: controllerInteraction)
self.displayNodeDidLoad()

View File

@ -3,16 +3,19 @@ import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
public final class GalleryControllerInteraction {
public let presentController: (ViewController, ViewControllerPresentationArguments?) -> Void
public let dismissController: () -> Void
public let replaceRootController: (ViewController, Promise<Bool>?) -> Void
public let editMedia: (MessageId) -> Void
public init(presentController: @escaping (ViewController, ViewControllerPresentationArguments?) -> Void, dismissController: @escaping () -> Void, replaceRootController: @escaping (ViewController, Promise<Bool>?) -> Void) {
public init(presentController: @escaping (ViewController, ViewControllerPresentationArguments?) -> Void, dismissController: @escaping () -> Void, replaceRootController: @escaping (ViewController, Promise<Bool>?) -> Void, editMedia: @escaping (MessageId) -> Void) {
self.presentController = presentController
self.dismissController = dismissController
self.replaceRootController = replaceRootController
self.editMedia = editMedia
}
}

View File

@ -125,9 +125,15 @@ public final class GalleryThumbnailContainerNode: ASDisplayNode, UIScrollViewDel
self.items = items
self.itemNodes = itemNodes
}
var updatedIndexOnly = false
if let centralIndexAndProgress = self.centralIndexAndProgress, centralIndexAndProgress.0 != centralIndex, centralIndexAndProgress.1 == progress {
updatedIndexOnly = true
}
self.centralIndexAndProgress = (centralIndex, progress)
if let size = self.currentLayout {
self.updateLayout(size: size, transition: .immediate)
self.updateLayout(size: size, transition: updatedIndexOnly ? .animated(duration: 0.2, curve: .spring) : .immediate)
}
}

View File

@ -530,7 +530,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
let mediaManager = item.context.sharedContext.mediaManager
let videoNode = UniversalVideoNode(postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .gallery)
let videoScale: CGFloat
if item.content is WebEmbedVideoContent {
videoScale = 1.0
@ -578,6 +577,8 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
self.requiresDownload = true
var mediaFileStatus: Signal<MediaResourceStatus?, NoError> = .single(nil)
var hintSeekable = false
if let contentInfo = item.contentInfo, case let .message(message) = contentInfo {
if Namespaces.Message.allScheduled.contains(message.id.namespace) {
disablePictureInPicture = true
@ -611,6 +612,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
}
if let file = file {
for attribute in file.attributes {
if case let .Video(duration, _, _) = attribute, duration >= 30 {
hintSeekable = true
break
}
}
let status = messageMediaFileStatus(context: item.context, messageId: message.id, file: file)
if !isWebpage {
self.scrubberView.setFetchStatusSignal(status, strings: self.presentationData.strings, decimalSeparator: self.presentationData.dateTimeFormat.decimalSeparator, fileSize: file.size)
@ -634,7 +641,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
var initialBuffering = false
var playing = false
var isPaused = true
var seekable = false
var seekable = hintSeekable
var hasStarted = false
var displayProgress = true
if let value = value {
@ -688,7 +695,9 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
strongSelf.controlsTimer = nil
}
}
seekable = value.duration >= 30.0
if !value.duration.isZero {
seekable = value.duration >= 30.0
}
}
if strongSelf.isCentral && playing && strongSelf.previousPlaying != true && !disablePlayerControls {
@ -749,7 +758,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
if hasStarted || strongSelf.didPause {
strongSelf.footerContentNode.content = .playback(paused: true, seekable: seekable)
} else if let fetchStatus = fetchStatus, !strongSelf.requiresDownload {
strongSelf.footerContentNode.content = .fetch(status: fetchStatus)
strongSelf.footerContentNode.content = .fetch(status: fetchStatus, seekable: seekable)
}
} else {
strongSelf.footerContentNode.content = .playback(paused: false, seekable: seekable)
@ -773,11 +782,17 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
self._rightBarButtonItems.set(.single(barButtonItems))
videoNode.playbackCompleted = { [weak videoNode] in
videoNode.playbackCompleted = { [weak self, weak videoNode] in
Queue.mainQueue().async {
item.playbackCompleted()
if !isAnimated {
if let strongSelf = self, !isAnimated {
videoNode?.seek(0.0)
if strongSelf.actionAtEnd == .stop {
strongSelf.updateControlsVisibility(true)
strongSelf.controlsTimer?.invalidate()
strongSelf.controlsTimer = nil
}
}
}
}

View File

@ -208,6 +208,7 @@ public final class SecretMediaPreviewController: ViewController {
}, dismissController: { [weak self] in
self?.dismiss(forceAway: true)
}, replaceRootController: { _, _ in
}, editMedia: { _ in
})
self.displayNode = SecretMediaPreviewControllerNode(controllerInteraction: controllerInteraction)
self.displayNodeDidLoad()

View File

@ -358,12 +358,13 @@ public class InstantPageGalleryController: ViewController, StandalonePresentable
if let strongSelf = self {
strongSelf.present(controller, in: .window(.root), with: arguments, blockInteraction: true)
}
}, dismissController: { [weak self] in
self?.dismiss(forceAway: true)
}, replaceRootController: { [weak self] controller, ready in
if let strongSelf = self {
strongSelf.replaceRootController(controller, ready)
}
}, dismissController: { [weak self] in
self?.dismiss(forceAway: true)
}, replaceRootController: { [weak self] controller, ready in
if let strongSelf = self {
strongSelf.replaceRootController(controller, ready)
}
}, editMedia: { _ in
})
self.displayNode = GalleryControllerNode(controllerInteraction: controllerInteraction)
self.displayNodeDidLoad()

View File

@ -50,4 +50,6 @@
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context items:(NSArray *)items focusItem:(id<TGModernGalleryItem>)focusItem selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext hasCaptions:(bool)hasCaptions allowCaptionEntities:(bool)allowCaptionEntities hasTimer:(bool)hasTimer onlyCrop:(bool)onlyCrop inhibitDocumentCaptions:(bool)inhibitDocumentCaptions hasSelectionPanel:(bool)hasSelectionPanel hasCamera:(bool)hasCamera recipientName:(NSString *)recipientName;
- (void)presentPhotoEditorForItem:(id<TGModernGalleryEditableItem>)item tab:(TGPhotoEditorTab)tab;
@end

View File

@ -4,6 +4,6 @@
+ (void)presentWithContext:(id<LegacyComponentsContext>)context parentController:(TGViewController *)parentController image:(UIImage *)image video:(NSURL *)video didFinishWithImage:(void (^)(UIImage *image))didFinishWithImage didFinishWithVideo:(void (^)(UIImage *image, NSURL *url, TGVideoEditAdjustments *adjustments))didFinishWithVideo dismissed:(void (^)(void))dismissed;
+ (void)presentWithContext:(id<LegacyComponentsContext>)context controller:(TGViewController *)controller caption:(NSString *)caption entities:(NSArray *)entities withItem:(id<TGMediaEditableItem, TGMediaSelectableItem>)item recipientName:(NSString *)recipientName stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext completion:(void (^)(id<TGMediaEditableItem>, TGMediaEditingContext *))completion dismissed:(void (^)())dismissed;
+ (void)presentWithContext:(id<LegacyComponentsContext>)context controller:(TGViewController *)controller caption:(NSString *)caption entities:(NSArray *)entities withItem:(id<TGMediaEditableItem, TGMediaSelectableItem>)item paint:(bool)paint recipientName:(NSString *)recipientName stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext completion:(void (^)(id<TGMediaEditableItem>, TGMediaEditingContext *))completion dismissed:(void (^)())dismissed;
@end

View File

@ -69,7 +69,7 @@
+ (UIImage *)paintIcon
{
return TGTintedImage([UIImage imageNamed:@"Editor/Drawing"], [self toolbarIconColor]);
return TGTintedImage([UIImage imageNamed:@"Editor/BrushSelectedPen"], [self toolbarIconColor]);
}
+ (UIImage *)stickerIcon

View File

@ -102,7 +102,7 @@
}
}
+ (void)presentWithContext:(id<LegacyComponentsContext>)context controller:(TGViewController *)controller caption:(NSString *)caption entities:(NSArray *)entities withItem:(id<TGMediaEditableItem, TGMediaSelectableItem>)item recipientName:(NSString *)recipientName stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext completion:(void (^)(id<TGMediaEditableItem>, TGMediaEditingContext *))completion dismissed:(void (^)())dismissed
+ (void)presentWithContext:(id<LegacyComponentsContext>)context controller:(TGViewController *)controller caption:(NSString *)caption entities:(NSArray *)entities withItem:(id<TGMediaEditableItem, TGMediaSelectableItem>)item paint:(bool)paint recipientName:(NSString *)recipientName stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext completion:(void (^)(id<TGMediaEditableItem>, TGMediaEditingContext *))completion dismissed:(void (^)())dismissed
{
id<LegacyComponentsOverlayWindowManager> windowManager = [context makeOverlayWindowManager];
id<LegacyComponentsContext> windowContext = [windowManager context];
@ -173,14 +173,6 @@
completion(item.asset, editingContext);
[strongController dismissWhenReadyAnimated:true];
/*[UIView animateWithDuration:0.3f delay:0.0f options:(7 << 16) animations:^
{
strongController.view.frame = CGRectOffset(strongController.view.frame, 0, strongController.view.frame.size.height);
} completion:^(__unused BOOL finished)
{
[strongController dismiss];
}];*/
};
galleryController.beginTransitionIn = ^UIView *(__unused TGMediaPickerGalleryItem *item, __unused TGModernGalleryItemView *itemView)
@ -207,10 +199,14 @@
dismissed();
}
};
TGOverlayControllerWindow *controllerWindow = [[TGOverlayControllerWindow alloc] initWithManager:windowManager parentController:controller contentController:galleryController];
controllerWindow.hidden = false;
galleryController.view.clipsToBounds = true;
// if (paint) {
// [model presentPhotoEditorForItem:galleryItem tab:TGPhotoEditorPaintTab];
// }
}
@end

View File

@ -59,6 +59,60 @@ public enum LegacyAttachmentMenuMediaEditing {
case file
}
public func legacyMediaEditor(context: AccountContext, peer: Peer, media: AnyMediaReference, initialCaption: String, presentStickers: @escaping (@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, present: @escaping (ViewController, Any?) -> Void) {
let _ = (fetchMediaData(context: context, postbox: context.account.postbox, mediaReference: media)
|> deliverOnMainQueue).start(next: { (value, isImage) in
guard case let .data(data) = value, data.complete else {
return
}
let item: TGMediaEditableItem & TGMediaSelectableItem
if let image = UIImage(contentsOfFile: data.path) {
item = TGCameraCapturedPhoto(existing: image)
} else {
item = TGCameraCapturedVideo(url: URL(fileURLWithPath: data.path))
}
let paintStickersContext = LegacyPaintStickersContext(context: context)
paintStickersContext.presentStickersController = { completion in
return presentStickers({ file, animated, view, rect in
let coder = PostboxEncoder()
coder.encodeRootObject(file)
completion?(coder.makeData(), animated, view, rect)
})
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let recipientName = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil)
legacyController.blocksBackgroundWhenInOverlay = true
legacyController.acceptsFocusWhenInOverlay = true
legacyController.statusBar.statusBarStyle = .Ignore
legacyController.controllerLoaded = { [weak legacyController] in
legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true
}
let emptyController = LegacyEmptyController(context: legacyController.context)!
emptyController.navigationBarShouldBeHidden = true
let navigationController = makeLegacyNavigationController(rootController: emptyController)
navigationController.setNavigationBarHidden(true, animated: false)
legacyController.bind(controller: navigationController)
legacyController.enableSizeClassSignal = true
present(legacyController, nil)
TGPhotoVideoEditor.present(with: legacyController.context, controller: emptyController, caption: initialCaption, entities: [], withItem: item, paint: true, recipientName: recipientName, stickersContext: paintStickersContext, completion: { result, editingContext in
let intent: TGMediaAssetsControllerIntent = TGMediaAssetsControllerSendMediaIntent
let signals = TGCameraController.resultSignals(for: nil, editingContext: editingContext, currentItem: result as! TGMediaSelectableItem, storeAssets: false, saveEditedPhotos: false, descriptionGenerator: legacyAssetPickerItemGenerator())
sendMessagesWithSignals(signals, false, 0)
}, dismissed: { [weak legacyController] in
legacyController?.dismiss()
})
})
}
public func legacyAttachmentMenu(context: AccountContext, peer: Peer, chatLocation: ChatLocation, editMediaOptions: LegacyAttachmentMenuMediaEditing?, saveEditedPhotos: Bool, allowGrouping: Bool, hasSchedule: Bool, canSendPolls: Bool, presentationData: PresentationData, parentController: LegacyController, recentlyUsedInlineBots: [Peer], initialCaption: String, openGallery: @escaping () -> Void, openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void, openFileGallery: @escaping () -> Void, openWebSearch: @escaping () -> Void, openMap: @escaping () -> Void, openContacts: @escaping () -> Void, openPoll: @escaping () -> Void, presentSelectionLimitExceeded: @escaping () -> Void, presentCantSendMultipleFiles: @escaping () -> Void, presentSchedulePicker: @escaping (@escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, selectRecentlyUsedInlineBot: @escaping (Peer) -> Void, presentStickers: @escaping (@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?, present: @escaping (ViewController, Any?) -> Void) -> TGMenuSheetController {
let defaultVideoPreset = defaultVideoPresetForContext(context)
UserDefaults.standard.set(defaultVideoPreset.rawValue as NSNumber, forKey: "TG_preferredVideoPreset_v0")
@ -261,45 +315,14 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, chatLocati
present(legacyController, nil)
TGPhotoVideoEditor.present(with: legacyController.context, controller: emptyController, caption: "", entities: [], withItem: item, recipientName: recipientName, stickersContext: paintStickersContext, completion: { result, editingContext in
TGPhotoVideoEditor.present(with: legacyController.context, controller: emptyController, caption: "", entities: [], withItem: item, paint: false, recipientName: recipientName, stickersContext: paintStickersContext, completion: { result, editingContext in
let intent: TGMediaAssetsControllerIntent = TGMediaAssetsControllerSendMediaIntent
let signals = TGCameraController.resultSignals(for: nil, editingContext: editingContext, currentItem: result as! TGMediaSelectableItem, storeAssets: false, saveEditedPhotos: false, descriptionGenerator: legacyAssetPickerItemGenerator())
sendMessagesWithSignals(signals, false, 0)
/*
[TGCameraController resultSignalsForSelectionContext:nil editingContext:editingContext currentItem:result storeAssets:false saveEditedPhotos:false descriptionGenerator:^id(id result, NSString *caption, NSArray *entities, NSString *hash)
{
__strong TGModernConversationController *strongSelf = weakSelf;
if (strongSelf == nil)
return nil;
NSDictionary *desc = [strongSelf _descriptionForItem:result caption:caption entities:entities hash:hash allowRemoteCache:allowRemoteCache];
return [strongSelf _descriptionForReplacingMedia:desc message:message];
}]]
*/
//let signals = TGMediaAssetsController.resultSignals(for: nil, editingContext: editingContext, intent: intent, currentItem: result, storeAssets: true, useMediaCache: false, descriptionGenerator: legacyAssetPickerItemGenerator(), saveEditedPhotos: saveEditedPhotos)
//sendMessagesWithSignals(signals, silentPosting, scheduleTime)
}, dismissed: { [weak legacyController] in
legacyController?.dismiss()
})
})
/*
bool allowRemoteCache = [strongSelf->_companion controllerShouldCacheServerAssets];
[TGPhotoVideoEditor presentWithContext:[TGLegacyComponentsContext shared] controller:strongSelf caption:text entities:entities withItem:item recipientName:[strongSelf->_companion title] completion:^(id result, TGMediaEditingContext *editingContext)
{
[strongSelf _asyncProcessMediaAssetSignals:[TGCameraController resultSignalsForSelectionContext:nil editingContext:editingContext currentItem:result storeAssets:false saveEditedPhotos:false descriptionGenerator:^id(id result, NSString *caption, NSArray *entities, NSString *hash)
{
__strong TGModernConversationController *strongSelf = weakSelf;
if (strongSelf == nil)
return nil;
NSDictionary *desc = [strongSelf _descriptionForItem:result caption:caption entities:entities hash:hash allowRemoteCache:allowRemoteCache];
return [strongSelf _descriptionForReplacingMedia:desc message:message];
}]];
[strongSelf endMessageEditing:true];
}];
*/
})!
itemViews.append(editCurrentItem)
}

View File

@ -6,32 +6,6 @@ import LegacyComponents
import TelegramPresentationData
import LegacyUI
public func presentLegacyAvatarEditor(theme: PresentationTheme, image: UIImage?, video: URL?, present: (ViewController, Any?) -> Void, imageCompletion: @escaping (UIImage) -> Void, videoCompletion: @escaping (UIImage, URL, TGVideoEditAdjustments?) -> Void) {
let legacyController = LegacyController(presentation: .custom, theme: theme)
legacyController.statusBar.statusBarStyle = .Ignore
let emptyController = LegacyEmptyController(context: legacyController.context)!
let navigationController = makeLegacyNavigationController(rootController: emptyController)
navigationController.setNavigationBarHidden(true, animated: false)
navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0)
legacyController.bind(controller: navigationController)
present(legacyController, nil)
TGPhotoVideoEditor.present(with: legacyController.context, parentController: emptyController, image: image, video: video, didFinishWithImage: { image in
if let image = image {
imageCompletion(image)
}
}, didFinishWithVideo: { image, asset, adjustments in
if let image = image {
// videoCompletion(image, url, adjustments)
}
}, dismissed: { [weak legacyController] in
legacyController?.dismiss()
})
}
public func presentLegacyAvatarPicker(holder: Atomic<NSObject?>, signup: Bool, theme: PresentationTheme, present: (ViewController, Any?) -> Void, openCurrent: (() -> Void)?, completion: @escaping (UIImage) -> Void, videoCompletion: @escaping (UIImage, Any?, TGVideoEditAdjustments?) -> Void = { _, _, _ in}) {
let legacyController = LegacyController(presentation: .custom, theme: theme)
legacyController.statusBar.statusBarStyle = .Ignore

View File

@ -177,12 +177,13 @@ class SecureIdDocumentGalleryController: ViewController, StandalonePresentableCo
if let strongSelf = self {
strongSelf.present(controller, in: .window(.root), with: arguments, blockInteraction: true)
}
}, dismissController: { [weak self] in
self?.dismiss(forceAway: true)
}, replaceRootController: { [weak self] controller, ready in
if let strongSelf = self {
strongSelf.replaceRootController(controller, ready)
}
}, dismissController: { [weak self] in
self?.dismiss(forceAway: true)
}, replaceRootController: { [weak self] controller, ready in
if let strongSelf = self {
strongSelf.replaceRootController(controller, ready)
}
}, editMedia: { _ in
})
self.displayNode = GalleryControllerNode(controllerInteraction: controllerInteraction)
self.displayNodeDidLoad()

View File

@ -517,6 +517,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr
if let strongSelf = self {
strongSelf.replaceRootController(controller, ready)
}
}, editMedia: { _ in
})
self.displayNode = GalleryControllerNode(controllerInteraction: controllerInteraction)
self.displayNodeDidLoad()

View File

@ -522,15 +522,15 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting
let twoStepAuthDataValue = Promise<TwoStepVerificationAccessConfiguration?>(nil)
let hasTwoStepAuthDataValue = twoStepAuthDataValue.get()
|> map { data -> Bool? in
|> mapToSignal { data -> Signal<Bool?, NoError> in
if let data = data {
if case .set = data {
return true
return .single(true)
} else {
return false
return .single(false)
}
} else {
return nil
return .complete()
}
}

View File

@ -312,6 +312,7 @@ public class WallpaperGalleryController: ViewController {
}, dismissController: { [weak self] in
self?.dismiss(forceAway: true)
}, replaceRootController: { controller, ready in
}, editMedia: { _ in
})
self.displayNode = WallpaperGalleryControllerNode(controllerInteraction: controllerInteraction, pageGap: 0.0)
self.displayNodeDidLoad()

View File

@ -296,6 +296,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-374917894] = { return Api.PhotoSize.parse_photoCachedSize($0) }
dict[-525288402] = { return Api.PhotoSize.parse_photoStrippedSize($0) }
dict[1520986705] = { return Api.PhotoSize.parse_photoSizeProgressive($0) }
dict[-668906175] = { return Api.PhotoSize.parse_photoPathSize($0) }
dict[-244016606] = { return Api.messages.Stickers.parse_stickersNotModified($0) }
dict[-463889475] = { return Api.messages.Stickers.parse_stickers($0) }
dict[-1096616924] = { return Api.GlobalPrivacySettings.parse_globalPrivacySettings($0) }

View File

@ -9467,6 +9467,7 @@ public extension Api {
case photoCachedSize(type: String, location: Api.FileLocation, w: Int32, h: Int32, bytes: Buffer)
case photoStrippedSize(type: String, bytes: Buffer)
case photoSizeProgressive(type: String, location: Api.FileLocation, w: Int32, h: Int32, sizes: [Int32])
case photoPathSize(type: String, bytes: Buffer)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
@ -9517,6 +9518,13 @@ public extension Api {
serializeInt32(item, buffer: buffer, boxed: false)
}
break
case .photoPathSize(let type, let bytes):
if boxed {
buffer.appendInt32(-668906175)
}
serializeString(type, buffer: buffer, boxed: false)
serializeBytes(bytes, buffer: buffer, boxed: false)
break
}
}
@ -9532,6 +9540,8 @@ public extension Api {
return ("photoStrippedSize", [("type", type), ("bytes", bytes)])
case .photoSizeProgressive(let type, let location, let w, let h, let sizes):
return ("photoSizeProgressive", [("type", type), ("location", location), ("w", w), ("h", h), ("sizes", sizes)])
case .photoPathSize(let type, let bytes):
return ("photoPathSize", [("type", type), ("bytes", bytes)])
}
}
@ -9637,6 +9647,20 @@ public extension Api {
return nil
}
}
public static func parse_photoPathSize(_ reader: BufferReader) -> PhotoSize? {
var _1: String?
_1 = parseString(reader)
var _2: Buffer?
_2 = parseBytes(reader)
let _c1 = _1 != nil
let _c2 = _2 != nil
if _c1 && _c2 {
return Api.PhotoSize.photoPathSize(type: _1!, bytes: _2!)
}
else {
return nil
}
}
}
public enum GlobalPrivacySettings: TypeConstructorDescription {

View File

@ -210,7 +210,7 @@ public class BoxedMessage: NSObject {
public class Serialization: NSObject, MTSerialization {
public func currentLayer() -> UInt {
return 120
return 121
}
public func parseMessage(_ data: Data!) -> Any! {

View File

@ -5,7 +5,7 @@ import SwiftSignalKit
import SyncCore
import MtProtoKit
func telegramStickerPachThumbnailRepresentationFromApiSize(datacenterId: Int32, size: Api.PhotoSize) -> TelegramMediaImageRepresentation? {
func telegramStickerPackThumbnailRepresentationFromApiSize(datacenterId: Int32, size: Api.PhotoSize) -> TelegramMediaImageRepresentation? {
switch size {
case let .photoCachedSize(_, location, w, h, _):
switch location {
@ -25,6 +25,8 @@ func telegramStickerPachThumbnailRepresentationFromApiSize(datacenterId: Int32,
let resource = CloudStickerPackThumbnailMediaResource(datacenterId: datacenterId, volumeId: volumeId, localId: localId)
return TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: sizes)
}
case let .photoPathSize(_, data):
return nil
case .photoStrippedSize:
return nil
case .photoSizeEmpty:
@ -49,7 +51,7 @@ extension StickerPackCollectionInfo {
var thumbnailRepresentation: TelegramMediaImageRepresentation?
if let thumb = thumb, let thumbDcId = thumbDcId {
thumbnailRepresentation = telegramStickerPachThumbnailRepresentationFromApiSize(datacenterId: thumbDcId, size: thumb)
thumbnailRepresentation = telegramStickerPackThumbnailRepresentationFromApiSize(datacenterId: thumbDcId, size: thumb)
}
self.init(id: ItemCollectionId(namespace: namespace, id: id), flags: setFlags, accessHash: accessHash, title: title, shortName: shortName, thumbnail: thumbnailRepresentation, hash: nHash, count: count)

View File

@ -143,6 +143,8 @@ func telegramMediaFileThumbnailRepresentationsFromApiSizes(datacenterId: Int32,
let resource = CloudDocumentSizeMediaResource(datacenterId: datacenterId, documentId: documentId, accessHash: accessHash, sizeSpec: type, volumeId: volumeId, localId: localId, fileReference: fileReference)
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: sizes))
}
case let .photoPathSize(_, data):
immediateThumbnailData = data.makeData()
case let .photoStrippedSize(_, data):
immediateThumbnailData = data.makeData()
case .photoSizeEmpty:

View File

@ -31,6 +31,8 @@ func telegramMediaImageRepresentationsFromApiSizes(datacenterId: Int32, photoId:
}
case let .photoStrippedSize(_, data):
immediateThumbnailData = data.makeData()
case .photoPathSize:
break
case .photoSizeEmpty:
break
}

View File

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

View File

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

View File

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

View File

@ -1,9 +1,9 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}
}

View File

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

View File

@ -615,6 +615,36 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
storedState = MediaPlaybackStoredState(timestamp: timestamp, playbackRate: .x1)
}
let _ = updateMediaPlaybackStoredStateInteractively(postbox: strongSelf.context.account.postbox, messageId: messageId, state: storedState).start()
}, editMedia: { [weak self] messageId in
guard let strongSelf = self else {
return
}
let _ = (strongSelf.context.account.postbox.transaction { transaction -> Message? in
return transaction.getMessage(messageId)
} |> deliverOnMainQueue).start(next: { [weak self] message in
guard let strongSelf = self, let message = message else {
return
}
var mediaReference: AnyMediaReference?
for m in message.media {
if let image = m as? TelegramMediaImage {
mediaReference = AnyMediaReference.standalone(media: image)
}
}
if let mediaReference = mediaReference, let peer = message.peers[message.id.peerId] {
legacyMediaEditor(context: strongSelf.context, peer: peer, media: mediaReference, initialCaption: message.text, presentStickers: { _ in return nil }, sendMessagesWithSignals: { [weak self] signals, _, _ in
if let strongSelf = self {
strongSelf.interfaceInteraction?.setupEditMessage(messageId, { _ in })
strongSelf.editMessageMediaWithLegacySignals(signals!)
}
}, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
})
}
})
})))
}, openPeer: { [weak self] id, navigation, fromMessage in
self?.openPeer(peerId: id, navigation: navigation, fromMessage: fromMessage)
@ -643,7 +673,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
break
}
}
let _ = combineLatest(queue: .mainQueue(), contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, context: strongSelf.context, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, selectAll: selectAll, interfaceInteraction: strongSelf.interfaceInteraction), loadedStickerPack(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, reference: .animatedEmoji, forceActualized: false), ApplicationSpecificNotice.getChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager)
let _ = combineLatest(queue: .mainQueue(), contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, context: strongSelf.context, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, selectAll: selectAll, interfaceInteraction: strongSelf.interfaceInteraction), loadedStickerPack(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, reference: .animatedEmoji, forceActualized: false), ApplicationSpecificNotice.getChatTextSelectionTips(accountManager: strongSelf.context.sharedContext.accountManager)
).start(next: { actions, animatedEmojiStickers, chatTextSelectionTips in
guard let strongSelf = self, !actions.isEmpty else {
return
@ -2275,6 +2305,45 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
strongSelf.push(messageStatsController(context: context, messageId: id, cachedPeerData: cachedPeerData))
})
}, editMessageMedia: { [weak self] messageId, draw in
guard let strongSelf = self else {
return
}
strongSelf.chatDisplayNode.dismissInput()
if draw {
let _ = (strongSelf.context.account.postbox.transaction { transaction -> Message? in
return transaction.getMessage(messageId)
} |> deliverOnMainQueue).start(next: { [weak self] message in
guard let strongSelf = self, let message = message else {
return
}
var mediaReference: AnyMediaReference?
for m in message.media {
if let image = m as? TelegramMediaImage {
mediaReference = AnyMediaReference.standalone(media: image)
}
}
if let mediaReference = mediaReference, let peer = message.peers[message.id.peerId] {
legacyMediaEditor(context: strongSelf.context, peer: peer, media: mediaReference, initialCaption: message.text, presentStickers: { _ in return nil }, sendMessagesWithSignals: { [weak self] signals, _, _ in
if let strongSelf = self {
strongSelf.interfaceInteraction?.setupEditMessage(messageId, { _ in })
strongSelf.editMessageMediaWithLegacySignals(signals!)
}
}, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
})
}
})
} else {
strongSelf.presentMediaPicker(fileMode: false, editingMedia: true, completion: { signals, _, _ in
self?.interfaceInteraction?.setupEditMessage(messageId, { _ in })
self?.editMessageMediaWithLegacySignals(signals)
})
}
}, requestMessageUpdate: { [weak self] id in
if let strongSelf = self {
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id)

View File

@ -116,6 +116,7 @@ public final class ChatControllerInteraction {
let openMessageReplies: (MessageId, Bool, Bool) -> Void
let openReplyThreadOriginalMessage: (Message) -> Void
let openMessageStats: (MessageId) -> Void
let editMessageMedia: (MessageId, Bool) -> Void
let requestMessageUpdate: (MessageId) -> Void
let cancelInteractiveKeyboardGestures: () -> Void
@ -203,6 +204,7 @@ public final class ChatControllerInteraction {
openMessageReplies: @escaping (MessageId, Bool, Bool) -> Void,
openReplyThreadOriginalMessage: @escaping (Message) -> Void,
openMessageStats: @escaping (MessageId) -> Void,
editMessageMedia: @escaping (MessageId, Bool) -> Void,
requestMessageUpdate: @escaping (MessageId) -> Void,
cancelInteractiveKeyboardGestures: @escaping () -> Void,
automaticMediaDownloadSettings: MediaAutoDownloadSettings,
@ -277,6 +279,7 @@ public final class ChatControllerInteraction {
self.openMessageReplies = openMessageReplies
self.openReplyThreadOriginalMessage = openReplyThreadOriginalMessage
self.openMessageStats = openMessageStats
self.editMessageMedia = editMessageMedia
self.requestMessageUpdate = requestMessageUpdate
self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures
@ -328,6 +331,7 @@ public final class ChatControllerInteraction {
}, openMessageReplies: { _, _, _ in
}, openReplyThreadOriginalMessage: { _ in
}, openMessageStats: { _ in
}, editMessageMedia: { _, _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -276,7 +276,7 @@ func updatedChatEditInterfaceMessageState(state: ChatPresentationInterfaceState,
return updated
}
func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, messages: [Message], controllerInteraction: ChatControllerInteraction?, selectAll: Bool, interfaceInteraction: ChatPanelInterfaceInteraction?) -> Signal<[ContextMenuItem], NoError> {
func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, messages: [Message], controllerInteraction: ChatControllerInteraction?, selectAll: Bool, interfaceInteraction: ChatPanelInterfaceInteraction?) -> Signal<[ContextMenuItem], NoError> {
guard let interfaceInteraction = interfaceInteraction, let controllerInteraction = controllerInteraction else {
return .single([])
}
@ -643,12 +643,79 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
}
if data.canEdit && !isPinnedMessages {
var mediaReference: AnyMediaReference?
for media in message.media {
if let image = media as? TelegramMediaImage, let _ = largestImageRepresentation(image.representations) {
mediaReference = ImageMediaReference.standalone(media: image).abstract
break
} else if let file = media as? TelegramMediaFile {
mediaReference = FileMediaReference.standalone(media: file).abstract
break
}
}
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_MessageDialogEdit, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
interfaceInteraction.setupEditMessage(messages[0].id, { transition in
f(.custom(transition))
})
}, action: { c, f in
if let _ = mediaReference?.media as? TelegramMediaImage {
var updatedItems: [ContextMenuItem] = []
updatedItems.append(.action(ContextMenuActionItem(text: message.text.isEmpty ? chatPresentationInterfaceState.strings.Conversation_AddCaption : chatPresentationInterfaceState.strings.Conversation_EditCaption, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Caption"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
interfaceInteraction.setupEditMessage(messages[0].id, { transition in
f(.custom(transition))
})
})))
updatedItems.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_EditThisPhoto, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Draw"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
controllerInteraction.editMessageMedia(messages[0].id, true)
f(.dismissWithoutContent)
})))
updatedItems.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ReplacePhoto, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Replace"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
controllerInteraction.editMessageMedia(messages[0].id, false)
f(.dismissWithoutContent)
})))
updatedItems.append(.separator)
updatedItems.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.ChatList_Context_Back, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor)
}, action: { c, _ in
c.setItems(contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: chatPresentationInterfaceState, context: context, messages: messages, controllerInteraction: controllerInteraction, selectAll: selectAll, interfaceInteraction: interfaceInteraction))
})))
c.setItems(.single(updatedItems))
} else if let _ = mediaReference?.media as? TelegramMediaFile {
var updatedItems: [ContextMenuItem] = []
updatedItems.append(.action(ContextMenuActionItem(text: message.text.isEmpty ? chatPresentationInterfaceState.strings.Conversation_AddCaption : chatPresentationInterfaceState.strings.Conversation_EditCaption, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Caption"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
interfaceInteraction.setupEditMessage(messages[0].id, { transition in
f(.custom(transition))
})
})))
updatedItems.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ReplaceFile, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Replace"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
controllerInteraction.editMessageMedia(messages[0].id, false)
f(.dismissWithoutContent)
})))
updatedItems.append(.separator)
updatedItems.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.ChatList_Context_Back, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor)
}, action: { c, _ in
c.setItems(contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState: chatPresentationInterfaceState, context: context, messages: messages, controllerInteraction: controllerInteraction, selectAll: selectAll, interfaceInteraction: interfaceInteraction))
})))
c.setItems(.single(updatedItems))
} else {
interfaceInteraction.setupEditMessage(messages[0].id, { transition in
f(.custom(transition))
})
}
})))
}

View File

@ -175,7 +175,7 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
private var currentSize: CGSize?
let imageNode: TransformImageNode
var animationNode: AnimatedStickerNode?
private var placeholderNode: ShimmerEffectNode?
private var placeholderNode: StickerShimmerEffectNode?
private var didSetUpAnimationNode = false
private var item: ChatMediaInputStickerGridItem?
@ -202,7 +202,8 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
override init() {
self.imageNode = TransformImageNode()
self.placeholderNode = ShimmerEffectNode()
self.placeholderNode = StickerShimmerEffectNode()
self.placeholderNode?.isUserInteractionEnabled = false
super.init()
@ -315,16 +316,16 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
if let placeholderNode = self.placeholderNode {
let placeholderFrame = CGRect(origin: CGPoint(x: floor((size.width - boundingSize.width) / 2.0), y: floor((size.height - boundingSize.height) / 2.0)), size: boundingSize)
placeholderNode.frame = CGRect(origin: CGPoint(), size: size)
placeholderNode.frame = placeholderFrame
let theme = item.theme
placeholderNode.update(backgroundColor: theme.chat.inputMediaPanel.stickersBackgroundColor, foregroundColor: theme.chat.inputMediaPanel.stickersSectionTextColor.blitOver(theme.chat.inputMediaPanel.stickersBackgroundColor, alpha: 0.1), shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3), shapes: [.roundedRect(rect: placeholderFrame, cornerRadius: 10.0)], size: bounds.size)
placeholderNode.update(backgroundColor: theme.chat.inputMediaPanel.stickersBackgroundColor, foregroundColor: theme.chat.inputMediaPanel.stickersSectionTextColor.blitOver(theme.chat.inputMediaPanel.stickersBackgroundColor, alpha: 0.15), shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3), data: item.stickerItem.file.immediateThumbnailData, size: placeholderFrame.size)
}
}
override func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) {
if let placeholderNode = self.placeholderNode {
placeholderNode.updateAbsoluteRect(absoluteRect, within: containerSize)
placeholderNode.updateAbsoluteRect(CGRect(origin: CGPoint(x: absoluteRect.minX + placeholderNode.frame.minX, y: absoluteRect.minY + placeholderNode.frame.minY), size: placeholderNode.frame.size), within: containerSize)
}
}

View File

@ -143,6 +143,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
private let contextSourceNode: ContextExtractedContentContainingNode
private let containerNode: ContextControllerSourceNode
let imageNode: TransformImageNode
private var placeholderNode: StickerShimmerEffectNode?
private var animationNode: GenericAnimatedStickerNode?
private var didSetUpAnimationNode = false
private var isPlaying = false
@ -191,6 +192,9 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
self.imageNode = TransformImageNode()
self.dateAndStatusNode = ChatMessageDateAndStatusNode()
self.placeholderNode = StickerShimmerEffectNode()
self.placeholderNode?.isUserInteractionEnabled = false
super.init(layerBacked: false)
self.containerNode.shouldBegin = { [weak self] location in
@ -230,12 +234,28 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
}
}
}
var firstTime = true
self.imageNode.imageUpdated = { [weak self] image in
guard let strongSelf = self else {
return
}
if image != nil {
strongSelf.removePlaceholder(animated: !firstTime)
}
firstTime = false
}
self.imageNode.displaysAsynchronously = false
self.containerNode.addSubnode(self.contextSourceNode)
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
self.addSubnode(self.containerNode)
self.contextSourceNode.contentNode.addSubnode(self.imageNode)
if let placeholderNode = self.placeholderNode {
self.contextSourceNode.contentNode.addSubnode(placeholderNode)
}
self.contextSourceNode.contentNode.addSubnode(self.dateAndStatusNode)
self.dateAndStatusNode.openReactions = { [weak self] in
@ -255,6 +275,20 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
fatalError("init(coder:) has not been implemented")
}
private func removePlaceholder(animated: Bool) {
if let placeholderNode = self.placeholderNode {
self.placeholderNode = nil
if !animated {
placeholderNode.removeFromSupernode()
} else {
placeholderNode.alpha = 0.0
placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak placeholderNode] _ in
placeholderNode?.removeFromSupernode()
})
}
}
}
override func didLoad() {
super.didLoad()
@ -388,7 +422,11 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
}
if let animationNode = self.animationNode, !self.animateGreeting {
self.contextSourceNode.contentNode.insertSubnode(animationNode, aboveSubnode: self.imageNode)
if let placeholderNode = self.placeholderNode {
self.contextSourceNode.contentNode.insertSubnode(animationNode, aboveSubnode: placeholderNode)
} else {
self.contextSourceNode.contentNode.insertSubnode(animationNode, aboveSubnode: self.imageNode)
}
}
}
@ -520,6 +558,20 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
self.updateVisibility()
}
private var absoluteRect: (CGRect, CGSize)?
override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
self.absoluteRect = (rect, containerSize)
if !self.contextSourceNode.isExtractedToContextPreview {
var rect = rect
rect.origin.y = containerSize.height - rect.maxY + self.insets.top
if let placeholderNode = self.placeholderNode {
placeholderNode.updateAbsoluteRect(CGRect(origin: CGPoint(x: rect.minX + placeholderNode.frame.minX, y: rect.minY + placeholderNode.frame.minY), size: placeholderNode.frame.size), within: containerSize)
}
}
}
override func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, Bool) -> Void) {
let displaySize = CGSize(width: 184.0, height: 184.0)
let telegramFile = self.telegramFile
@ -903,6 +955,20 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
strongSelf.imageNode.frame = updatedContentFrame
let animationNodeFrame = updatedContentFrame.insetBy(dx: imageInset, dy: imageInset)
var file: TelegramMediaFile?
if let emojiFile = emojiFile {
file = emojiFile
} else if let telegramFile = telegramFile {
file = telegramFile
}
if let immediateThumbnailData = file?.immediateThumbnailData, let placeholderNode = strongSelf.placeholderNode {
placeholderNode.update(backgroundColor: nil, foregroundColor: UIColor(rgb: 0x748391, alpha: 0.2), shimmeringColor: UIColor(rgb: 0x748391, alpha: 0.35), data: immediateThumbnailData, size: animationNodeFrame.size)
placeholderNode.frame = animationNodeFrame
strongSelf.animationNode?.isHidden = true
}
if let animationNode = strongSelf.animationNode, let parentNode = strongSelf.greetingStickerParentNode, strongSelf.animateGreeting {
strongSelf.animateGreeting = false
@ -1117,7 +1183,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
switch recognizer.state {
case .ended:
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
if let action = self.gestureRecognized(gesture: gesture, location: location, recognizer: nil) {
if let action = self.gestureRecognized(gesture: gesture, location: location, recognizer: recognizer) {
if case .doubleTap = gesture {
self.containerNode.cancelGesture()
}
@ -1224,7 +1290,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
item.controllerInteraction.displayDiceTooltip(dice)
})
} else if let _ = self.emojiFile {
if let animationNode = self.animationNode as? AnimatedStickerNode {
if let animationNode = self.animationNode as? AnimatedStickerNode, let _ = recognizer {
var startTime: Signal<Double, NoError>
var shouldPlay = false
if !animationNode.isPlaying {
@ -1248,86 +1314,63 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
}
if let text = self.item?.message.text, let firstScalar = text.unicodeScalars.first {
if beatingHearts.contains(firstScalar.value) || firstScalar.value == peach {
return .optionalAction({
if shouldPlay {
animationNode.play()
}
return .optionalAction({
let _ = startTime.start(next: { [weak self] time in
let _ = (appConfiguration
|> deliverOnMainQueue).start(next: { [weak self, weak animationNode] appConfiguration in
guard let strongSelf = self else {
return
}
var haptic: EmojiHaptic
if let current = strongSelf.haptic {
haptic = current
} else {
if beatingHearts.contains(firstScalar.value) {
haptic = HeartbeatHaptic()
} else {
haptic = PeachHaptic()
}
haptic.enabled = true
strongSelf.haptic = haptic
}
if !haptic.active {
haptic.start(time: time)
}
})
})
} else {
return .optionalAction({
if shouldPlay {
let _ = (appConfiguration
|> deliverOnMainQueue).start(next: { [weak self, weak animationNode] appConfiguration in
guard let strongSelf = self else {
return
}
let emojiSounds = AnimatedEmojiSoundsConfiguration.with(appConfiguration: appConfiguration, account: item.context.account)
for (emoji, file) in emojiSounds.sounds {
if emoji.strippedEmoji == text.strippedEmoji {
let mediaManager = item.context.sharedContext.mediaManager
let mediaPlayer = MediaPlayer(audioSessionManager: mediaManager.audioSession, postbox: item.context.account.postbox, resourceReference: .standalone(resource: file.resource), streamable: .none, video: false, preferSoftwareDecoding: false, enableSound: true, fetchAutomatically: true, ambient: true)
mediaPlayer.togglePlayPause()
mediaPlayer.actionAtEnd = .action({ [weak self] in
self?.mediaPlayer = nil
})
strongSelf.mediaPlayer = mediaPlayer
strongSelf.mediaStatusDisposable.set((mediaPlayer.status
|> deliverOnMainQueue).start(next: { [weak self, weak animationNode] status in
if let strongSelf = self {
if firstScalar.value == coffin {
var haptic: EmojiHaptic
if let current = strongSelf.haptic {
haptic = current
} else {
let emojiSounds = AnimatedEmojiSoundsConfiguration.with(appConfiguration: appConfiguration, account: item.context.account)
for (emoji, file) in emojiSounds.sounds {
if emoji.strippedEmoji == text.strippedEmoji {
let mediaManager = item.context.sharedContext.mediaManager
let mediaPlayer = MediaPlayer(audioSessionManager: mediaManager.audioSession, postbox: item.context.account.postbox, resourceReference: .standalone(resource: file.resource), streamable: .none, video: false, preferSoftwareDecoding: false, enableSound: true, fetchAutomatically: true, ambient: true)
mediaPlayer.togglePlayPause()
mediaPlayer.actionAtEnd = .action({ [weak self] in
self?.mediaPlayer = nil
})
strongSelf.mediaPlayer = mediaPlayer
strongSelf.mediaStatusDisposable.set((mediaPlayer.status
|> deliverOnMainQueue).start(next: { [weak self, weak animationNode] status in
if let strongSelf = self {
if firstScalar.value == coffin || firstScalar.value == peach {
var haptic: EmojiHaptic
if let current = strongSelf.haptic {
haptic = current
} else {
if beatingHearts.contains(firstScalar.value) {
haptic = HeartbeatHaptic()
} else if firstScalar.value == coffin {
haptic = CoffinHaptic()
haptic.enabled = true
strongSelf.haptic = haptic
}
if !haptic.active {
haptic.start(time: 0.0)
} else {
haptic = PeachHaptic()
}
haptic.enabled = true
strongSelf.haptic = haptic
}
switch status.status {
case .playing:
animationNode?.play()
strongSelf.mediaStatusDisposable.set(nil)
default:
break
if !haptic.active {
haptic.start(time: 0.0)
}
}
}))
return
}
switch status.status {
case .playing:
animationNode?.play()
strongSelf.mediaStatusDisposable.set(nil)
default:
break
}
}
}))
return
}
animationNode?.play()
})
}
})
}
}
animationNode?.play()
})
}
})
}
}
}

View File

@ -248,6 +248,24 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
title = nil
messageText = rawText
}
case .video:
let rawText = presentationData.strings.PUSH_CHANNEL_MESSAGE_VIDEOS(Int32(item.messages.count), peer.displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder), Int32(item.messages.count))
if let index = rawText.firstIndex(of: "|") {
title = String(rawText[rawText.startIndex ..< index])
messageText = String(rawText[rawText.index(after: index)...])
} else {
title = nil
messageText = rawText
}
case .file:
let rawText = presentationData.strings.PUSH_CHANNEL_MESSAGE_DOCS(Int32(item.messages.count), peer.displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder), Int32(item.messages.count))
if let index = rawText.firstIndex(of: "|") {
title = String(rawText[rawText.startIndex ..< index])
messageText = String(rawText[rawText.index(after: index)...])
} else {
title = nil
messageText = rawText
}
default:
let rawText = presentationData.strings.PUSH_CHANNEL_MESSAGES(Int32(item.messages.count), peer.displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder), Int32(item.messages.count))
if let index = rawText.firstIndex(of: "|") {
@ -272,6 +290,24 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
title = nil
messageText = rawText
}
case .video:
let rawText = presentationData.strings.PUSH_CHAT_MESSAGE_VIDEOS(Int32(item.messages.count), author.compactDisplayTitle, peer.displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder), Int32(item.messages.count))
if let index = rawText.firstIndex(of: "|") {
title = String(rawText[rawText.startIndex ..< index])
messageText = String(rawText[rawText.index(after: index)...])
} else {
title = nil
messageText = rawText
}
case .file:
let rawText = presentationData.strings.PUSH_CHAT_MESSAGE_DOCS(Int32(item.messages.count), author.compactDisplayTitle, peer.displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder), Int32(item.messages.count))
if let index = rawText.firstIndex(of: "|") {
title = String(rawText[rawText.startIndex ..< index])
messageText = String(rawText[rawText.index(after: index)...])
} else {
title = nil
messageText = rawText
}
default:
let rawText = presentationData.strings.PUSH_CHAT_MESSAGES(Int32(item.messages.count), author.compactDisplayTitle, peer.displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder), Int32(item.messages.count))
if let index = rawText.firstIndex(of: "|") {
@ -293,6 +329,24 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
title = nil
messageText = rawText
}
case .video:
let rawText = presentationData.strings.PUSH_MESSAGE_VIDEOS(Int32(item.messages.count), peer.displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder), Int32(item.messages.count))
if let index = rawText.firstIndex(of: "|") {
title = String(rawText[rawText.startIndex ..< index])
messageText = String(rawText[rawText.index(after: index)...])
} else {
title = nil
messageText = rawText
}
case .file:
let rawText = presentationData.strings.PUSH_MESSAGE_DOCS(Int32(item.messages.count), peer.displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder), Int32(item.messages.count))
if let index = rawText.firstIndex(of: "|") {
title = String(rawText[rawText.startIndex ..< index])
messageText = String(rawText[rawText.index(after: index)...])
} else {
title = nil
messageText = rawText
}
default:
let rawText = presentationData.strings.PUSH_MESSAGES(Int32(item.messages.count), peer.displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder), Int32(item.messages.count))
if let index = rawText.firstIndex(of: "|") {

View File

@ -455,6 +455,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, openMessageReplies: { _, _, _ in
}, openReplyThreadOriginalMessage: { _ in
}, openMessageStats: { _ in
}, editMessageMedia: { _, _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,

View File

@ -148,6 +148,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode {
}, openMessageReplies: { _, _, _ in
}, openReplyThreadOriginalMessage: { _ in
}, openMessageStats: { _ in
}, editMessageMedia: { _, _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -114,7 +114,15 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
let controller = ShareController(context: params.context, subject: .media(.standalone(media: file)), immediateExternalShare: true)
params.present(controller, nil)
} else if let rootController = params.navigationController?.view.window?.rootViewController {
presentDocumentPreviewController(rootController: rootController, theme: presentationData.theme, strings: presentationData.strings, postbox: params.context.account.postbox, file: file)
let proceed = {
presentDocumentPreviewController(rootController: rootController, theme: presentationData.theme, strings: presentationData.strings, postbox: params.context.account.postbox, file: file)
}
if file.mimeType.contains("image/svg") {
let presentationData = params.context.sharedContext.currentPresentationData.with { $0 }
params.present(textAlertController(context: params.context, title: nil, text: presentationData.strings.OpenFile_PotentiallyDangerousContentAlert, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.OpenFile_Proceed, action: { proceed() })] ), nil)
} else {
proceed()
}
}
return true
case let .audio(file):
@ -217,9 +225,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
let path = params.context.account.postbox.mediaBox.completedResourcePath(media.resource)
var previewTheme: PresentationTheme?
if let path = path, let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) {
let startTime = CACurrentMediaTime()
previewTheme = makePresentationTheme(data: data)
print("time \(CACurrentMediaTime() - startTime)")
}
guard let theme = previewTheme else {

View File

@ -138,6 +138,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
}, openMessageReplies: { _, _, _ in
}, openReplyThreadOriginalMessage: { _ in
}, openMessageStats: { _ in
}, editMessageMedia: { _, _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false))

View File

@ -1969,6 +1969,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
}, openMessageReplies: { _, _, _ in
}, openReplyThreadOriginalMessage: { _ in
}, openMessageStats: { _ in
}, editMessageMedia: { _, _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -1238,6 +1238,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}, openMessageReplies: { _, _, _ in
}, openReplyThreadOriginalMessage: { _ in
}, openMessageStats: { _ in
}, editMessageMedia: { _, _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -0,0 +1,605 @@
import Foundation
import AsyncDisplayKit
import Display
import Postbox
import TelegramPresentationData
import GZip
private final class ShimmerEffectForegroundNode: ASDisplayNode {
private var currentBackgroundColor: UIColor?
private var currentForegroundColor: UIColor?
private let imageNodeContainer: ASDisplayNode
private let imageNode: ASImageNode
private var absoluteLocation: (CGRect, CGSize)?
private var isCurrentlyInHierarchy = false
private var shouldBeAnimating = false
override init() {
self.imageNodeContainer = ASDisplayNode()
self.imageNodeContainer.isLayerBacked = true
self.imageNode = ASImageNode()
self.imageNode.isLayerBacked = true
self.imageNode.displaysAsynchronously = false
self.imageNode.displayWithoutProcessing = true
self.imageNode.contentMode = .scaleToFill
super.init()
self.isLayerBacked = true
self.clipsToBounds = true
self.imageNodeContainer.addSubnode(self.imageNode)
self.addSubnode(self.imageNodeContainer)
}
override func didEnterHierarchy() {
super.didEnterHierarchy()
self.isCurrentlyInHierarchy = true
self.updateAnimation()
}
override func didExitHierarchy() {
super.didExitHierarchy()
self.isCurrentlyInHierarchy = false
self.updateAnimation()
}
func update(backgroundColor: UIColor, foregroundColor: UIColor) {
if let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor) {
return
}
self.currentBackgroundColor = backgroundColor
self.currentForegroundColor = foregroundColor
let image = generateImage(CGSize(width: 320.0, height: 16.0), opaque: false, scale: 1.0, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(backgroundColor.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
context.clip(to: CGRect(origin: CGPoint(), size: size))
let transparentColor = foregroundColor.withAlphaComponent(0.0).cgColor
let peakColor = foregroundColor.cgColor
var locations: [CGFloat] = [0.0, 0.5, 1.0]
let colors: [CGColor] = [transparentColor, peakColor, transparentColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions())
})
self.imageNode.image = image
}
func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
if let absoluteLocation = self.absoluteLocation, absoluteLocation.0 == rect && absoluteLocation.1 == containerSize {
return
}
let sizeUpdated = self.absoluteLocation?.1 != containerSize
let frameUpdated = self.absoluteLocation?.0 != rect
self.absoluteLocation = (rect, containerSize)
if sizeUpdated {
if self.shouldBeAnimating {
self.imageNode.layer.removeAnimation(forKey: "shimmer")
self.addImageAnimation()
} else {
self.updateAnimation()
}
}
if frameUpdated {
self.imageNodeContainer.frame = CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize)
}
}
private func updateAnimation() {
let shouldBeAnimating = self.isCurrentlyInHierarchy && self.absoluteLocation != nil
if shouldBeAnimating != self.shouldBeAnimating {
self.shouldBeAnimating = shouldBeAnimating
if shouldBeAnimating {
self.addImageAnimation()
} else {
self.imageNode.layer.removeAnimation(forKey: "shimmer")
}
}
}
private func addImageAnimation() {
guard let containerSize = self.absoluteLocation?.1 else {
return
}
let gradientHeight: CGFloat = 320.0
self.imageNode.frame = CGRect(origin: CGPoint(x: -gradientHeight, y: 0.0), size: CGSize(width: gradientHeight, height: containerSize.height))
let animation = self.imageNode.layer.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.width + gradientHeight) as NSNumber, keyPath: "position.x", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 1.3 * 1.0, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true)
animation.repeatCount = Float.infinity
animation.beginTime = 1.0
self.imageNode.layer.add(animation, forKey: "shimmer")
}
}
class StickerShimmerEffectNode: ASDisplayNode {
private let backgroundNode: ASDisplayNode
private let effectNode: ShimmerEffectForegroundNode
private let foregroundNode: ASImageNode
private var maskView: UIImageView?
private var currentData: Data?
private var currentBackgroundColor: UIColor?
private var currentForegroundColor: UIColor?
private var currentShimmeringColor: UIColor?
private var currentSize = CGSize()
override init() {
self.backgroundNode = ASDisplayNode()
self.effectNode = ShimmerEffectForegroundNode()
self.foregroundNode = ASImageNode()
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.effectNode)
self.addSubnode(self.foregroundNode)
}
public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
self.effectNode.updateAbsoluteRect(rect, within: containerSize)
}
public func update(backgroundColor: UIColor?, foregroundColor: UIColor, shimmeringColor: UIColor, data: Data?, size: CGSize) {
if self.currentData == data, let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor), let currentShimmeringColor = self.currentShimmeringColor, currentShimmeringColor.isEqual(shimmeringColor), self.currentSize == size {
return
}
self.currentBackgroundColor = backgroundColor
self.currentForegroundColor = foregroundColor
self.currentShimmeringColor = shimmeringColor
self.currentData = data
self.currentSize = size
self.backgroundNode.backgroundColor = foregroundColor
self.effectNode.update(backgroundColor: backgroundColor == nil ? .clear : foregroundColor, foregroundColor: shimmeringColor)
let image = generateImage(size, rotatedContext: { size, context in
if let backgroundColor = backgroundColor {
context.setFillColor(backgroundColor.cgColor)
context.setBlendMode(.copy)
context.fill(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.clear.cgColor)
} else {
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.black.cgColor)
}
if let data = data, let unpackedData = TGGUnzipData(data, 5 * 1024 * 1024), let path = String(data: unpackedData, encoding: .utf8) {
let reader = PathDataReader(input: path)
let segments = reader.read()
var currentX: Double = 0.0
var currentY: Double = 0.0
let mul: Double = Double(size.width) / 512.0
for segment in segments {
switch segment.type {
case .M, .m:
let x = segment.data[0]
let y = segment.data[1]
if segment.isAbsolute() {
currentX = x
currentY = y
} else {
currentX += x
currentY += y
}
context.move(to: CGPoint(x: currentX * mul, y: currentY * mul))
case .L, .l:
let x = segment.data[0]
let y = segment.data[1]
let effectiveX: Double
let effectiveY: Double
if segment.isAbsolute() {
effectiveX = x
effectiveY = y
} else {
effectiveX = currentX + x
effectiveY = currentY + y
}
currentX = effectiveX
currentY = effectiveY
context.addLine(to: CGPoint(x: effectiveX * mul, y: effectiveY * mul))
case .C, .c:
let x1 = segment.data[0]
let y1 = segment.data[1]
let x2 = segment.data[2]
let y2 = segment.data[3]
let x = segment.data[4]
let y = segment.data[5]
let effectiveX1: Double
let effectiveY1: Double
let effectiveX2: Double
let effectiveY2: Double
let effectiveX: Double
let effectiveY: Double
if segment.isAbsolute() {
effectiveX1 = x1
effectiveY1 = y1
effectiveX2 = x2
effectiveY2 = y2
effectiveX = x
effectiveY = y
} else {
effectiveX1 = currentX + x1
effectiveY1 = currentY + y1
effectiveX2 = currentX + x2
effectiveY2 = currentY + y2
effectiveX = currentX + x
effectiveY = currentY + y
}
currentX = effectiveX
currentY = effectiveY
context.addCurve(to: CGPoint(x: effectiveX * mul, y: effectiveY * mul), control1: CGPoint(x: effectiveX1 * mul, y: effectiveY1 * mul), control2: CGPoint(x: effectiveX2 * mul, y: effectiveY2 * mul))
case .z:
context.fillPath()
default:
break
}
}
} else {
let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), byRoundingCorners: [.topLeft, .topRight, .bottomLeft, .bottomRight], cornerRadii: CGSize(width: 10.0, height: 10.0))
UIGraphicsPushContext(context)
path.fill()
UIGraphicsPopContext()
}
})
if backgroundColor == nil {
self.foregroundNode.image = nil
let maskView: UIImageView
if let current = self.maskView {
maskView = current
} else {
maskView = UIImageView()
maskView.frame = CGRect(origin: CGPoint(), size: size)
self.maskView = maskView
self.view.mask = maskView
}
} else {
self.foregroundNode.image = image
if let _ = self.maskView {
self.view.mask = nil
self.maskView = nil
}
}
self.maskView?.image = image
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size)
self.foregroundNode.frame = CGRect(origin: CGPoint(), size: size)
self.effectNode.frame = CGRect(origin: CGPoint(), size: size)
}
}
open class PathSegment: Equatable {
public enum SegmentType {
case M
case L
case C
case Q
case A
case z
case H
case V
case S
case T
case m
case l
case c
case q
case a
case h
case v
case s
case t
case E
case e
}
public let type: SegmentType
public let data: [Double]
public init(type: PathSegment.SegmentType = .M, data: [Double] = []) {
self.type = type
self.data = data
}
open func isAbsolute() -> Bool {
switch type {
case .M, .L, .H, .V, .C, .S, .Q, .T, .A, .E:
return true
default:
return false
}
}
public static func == (lhs: PathSegment, rhs: PathSegment) -> Bool {
return lhs.type == rhs.type && lhs.data == rhs.data
}
}
private class PathDataReader {
private let input: String
private var current: UnicodeScalar?
private var previous: UnicodeScalar?
private var iterator: String.UnicodeScalarView.Iterator
private static let spaces: Set<UnicodeScalar> = Set("\n\r\t ,".unicodeScalars)
init(input: String) {
self.input = input
self.iterator = input.unicodeScalars.makeIterator()
}
public func read() -> [PathSegment] {
readNext()
var segments = [PathSegment]()
while let array = readSegments() {
segments.append(contentsOf: array)
}
return segments
}
private func readSegments() -> [PathSegment]? {
if let type = readSegmentType() {
let argCount = getArgCount(segment: type)
if argCount == 0 {
return [PathSegment(type: type)]
}
var result = [PathSegment]()
let data: [Double]
if type == .a || type == .A {
data = readDataOfASegment()
} else {
data = readData()
}
var index = 0
var isFirstSegment = true
while index < data.count {
let end = index + argCount
if end > data.count {
break
}
var currentType = type
if type == .M && !isFirstSegment {
currentType = .L
}
if type == .m && !isFirstSegment {
currentType = .l
}
result.append(PathSegment(type: currentType, data: Array(data[index..<end])))
isFirstSegment = false
index = end
}
return result
}
return nil
}
private func readData() -> [Double] {
var data = [Double]()
while true {
skipSpaces()
if let value = readNum() {
data.append(value)
} else {
return data
}
}
}
private func readDataOfASegment() -> [Double] {
let argCount = getArgCount(segment: .A)
var data: [Double] = []
var index = 0
while true {
skipSpaces()
let value: Double?
let indexMod = index % argCount
if indexMod == 3 || indexMod == 4 {
value = readFlag()
} else {
value = readNum()
}
guard let doubleValue = value else {
return data
}
data.append(doubleValue)
index += 1
}
return data
}
private func skipSpaces() {
var currentCharacter = current
while let character = currentCharacter, Self.spaces.contains(character) {
currentCharacter = readNext()
}
}
private func readFlag() -> Double? {
guard let ch = current else {
return .none
}
readNext()
switch ch {
case "0":
return 0
case "1":
return 1
default:
return .none
}
}
fileprivate func readNum() -> Double? {
guard let ch = current else {
return .none
}
guard ch >= "0" && ch <= "9" || ch == "." || ch == "-" else {
return .none
}
var chars = [ch]
var hasDot = ch == "."
while let ch = readDigit(&hasDot) {
chars.append(ch)
}
var buf = ""
buf.unicodeScalars.append(contentsOf: chars)
guard let value = Double(buf) else {
return .none
}
return value
}
fileprivate func readDigit(_ hasDot: inout Bool) -> UnicodeScalar? {
if let ch = readNext() {
if (ch >= "0" && ch <= "9") || ch == "e" || (previous == "e" && ch == "-") {
return ch
} else if ch == "." && !hasDot {
hasDot = true
return ch
}
}
return nil
}
fileprivate func isNum(ch: UnicodeScalar, hasDot: inout Bool) -> Bool {
switch ch {
case "0"..."9":
return true
case ".":
if hasDot {
return false
}
hasDot = true
default:
return true
}
return false
}
@discardableResult
private func readNext() -> UnicodeScalar? {
previous = current
current = iterator.next()
return current
}
private func isAcceptableSeparator(_ ch: UnicodeScalar?) -> Bool {
if let ch = ch {
return "\n\r\t ,".contains(String(ch))
}
return false
}
private func readSegmentType() -> PathSegment.SegmentType? {
while true {
if let type = getPathSegmentType() {
readNext()
return type
}
if readNext() == nil {
return nil
}
}
}
fileprivate func getPathSegmentType() -> PathSegment.SegmentType? {
if let ch = current {
switch ch {
case "M":
return .M
case "m":
return .m
case "L":
return .L
case "l":
return .l
case "C":
return .C
case "c":
return .c
case "Q":
return .Q
case "q":
return .q
case "A":
return .A
case "a":
return .a
case "z", "Z":
return .z
case "H":
return .H
case "h":
return .h
case "V":
return .V
case "v":
return .v
case "S":
return .S
case "s":
return .s
case "T":
return .T
case "t":
return .t
default:
break
}
}
return nil
}
fileprivate func getArgCount(segment: PathSegment.SegmentType) -> Int {
switch segment {
case .H, .h, .V, .v:
return 1
case .M, .m, .L, .l, .T, .t:
return 2
case .S, .s, .Q, .q:
return 4
case .C, .c:
return 6
case .A, .a:
return 7
default:
return 0
}
}
}

View File

@ -222,12 +222,13 @@ class WebSearchGalleryController: ViewController {
if let strongSelf = self {
strongSelf.present(controller, in: .window(.root), with: arguments, blockInteraction: true)
}
}, dismissController: { [weak self] in
self?.dismiss(forceAway: true)
}, replaceRootController: { [weak self] controller, ready in
if let strongSelf = self {
strongSelf.replaceRootController(controller, ready)
}
}, dismissController: { [weak self] in
self?.dismiss(forceAway: true)
}, replaceRootController: { [weak self] controller, ready in
if let strongSelf = self {
strongSelf.replaceRootController(controller, ready)
}
}, editMedia: { _ in
})
self.displayNode = GalleryControllerNode(controllerInteraction: controllerInteraction)
self.displayNodeDidLoad()