mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Grouped music files
This commit is contained in:
parent
0da14b56dd
commit
814618bcf4
@ -5817,3 +5817,6 @@ Any member of this group will be able to see messages in the channel.";
|
|||||||
"Channel.DiscussionMessageUnavailable" = "Sorry, this post has been removed from the discussion group.";
|
"Channel.DiscussionMessageUnavailable" = "Sorry, this post has been removed from the discussion group.";
|
||||||
|
|
||||||
"Conversation.ContextViewStats" = "View Statistics";
|
"Conversation.ContextViewStats" = "View Statistics";
|
||||||
|
|
||||||
|
"ChatList.MessageMusic_1" = "1 Music File";
|
||||||
|
"ChatList.MessageMusic_any" = "%@ Music Files";
|
||||||
|
@ -104,13 +104,13 @@ func suggestDates(for string: String, strings: PresentationStrings, dateTimeForm
|
|||||||
|
|
||||||
let stringComponents = string.components(separatedBy: dateSeparator)
|
let stringComponents = string.components(separatedBy: dateSeparator)
|
||||||
if stringComponents.count < 3 {
|
if stringComponents.count < 3 {
|
||||||
for i in 0..<5 {
|
for i in 0..<8 {
|
||||||
if let date = calendar.date(byAdding: .year, value: -i, to: resultDate), date < now {
|
if let date = calendar.date(byAdding: .year, value: -i, to: resultDate), date < now, date > telegramReleaseDate {
|
||||||
let lowerDate = getLowerDate(for: resultDate)
|
let lowerDate = getLowerDate(for: resultDate)
|
||||||
result.append((lowerDate, date, nil))
|
result.append((lowerDate, date, nil))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if resultDate < now {
|
} else if resultDate < now, date > telegramReleaseDate {
|
||||||
let lowerDate = getLowerDate(for: resultDate)
|
let lowerDate = getLowerDate(for: resultDate)
|
||||||
result.append((lowerDate, resultDate, nil))
|
result.append((lowerDate, resultDate, nil))
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import LocalizedPeerData
|
|||||||
private enum MessageGroupType {
|
private enum MessageGroupType {
|
||||||
case photos
|
case photos
|
||||||
case videos
|
case videos
|
||||||
|
case music
|
||||||
case generic
|
case generic
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,6 +19,9 @@ private func singleMessageType(message: Message) -> MessageGroupType {
|
|||||||
if let _ = media as? TelegramMediaImage {
|
if let _ = media as? TelegramMediaImage {
|
||||||
return .photos
|
return .photos
|
||||||
} else if let file = media as? TelegramMediaFile {
|
} else if let file = media as? TelegramMediaFile {
|
||||||
|
if file.isMusic {
|
||||||
|
return .music
|
||||||
|
}
|
||||||
if file.isVideo && !file.isInstantVideo {
|
if file.isVideo && !file.isInstantVideo {
|
||||||
return .videos
|
return .videos
|
||||||
}
|
}
|
||||||
@ -80,6 +84,13 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
|
|||||||
messageText = strings.ChatList_MessageVideos(Int32(messages.count))
|
messageText = strings.ChatList_MessageVideos(Int32(messages.count))
|
||||||
textIsReady = true
|
textIsReady = true
|
||||||
}
|
}
|
||||||
|
case .music:
|
||||||
|
if !messageText.isEmpty {
|
||||||
|
textIsReady = true
|
||||||
|
} else {
|
||||||
|
messageText = strings.ChatList_MessageMusic(Int32(messages.count))
|
||||||
|
textIsReady = true
|
||||||
|
}
|
||||||
case .generic:
|
case .generic:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ public enum CheckNodeStyle {
|
|||||||
case plain
|
case plain
|
||||||
case overlay
|
case overlay
|
||||||
case navigation
|
case navigation
|
||||||
|
case compact
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class CheckNode: ASDisplayNode {
|
public final class CheckNode: ASDisplayNode {
|
||||||
@ -47,6 +48,9 @@ public final class CheckNode: ASDisplayNode {
|
|||||||
case .navigation:
|
case .navigation:
|
||||||
style = TGCheckButtonStyleGallery
|
style = TGCheckButtonStyleGallery
|
||||||
checkSize = CGSize(width: 39.0, height: 39.0)
|
checkSize = CGSize(width: 39.0, height: 39.0)
|
||||||
|
case .compact:
|
||||||
|
style = TGCheckButtonStyleCompact
|
||||||
|
checkSize = CGSize(width: 30.0, height: 30.0)
|
||||||
}
|
}
|
||||||
let checkView = TGCheckButtonView(style: style, pallete: TGCheckButtonPallete(defaultBackgroundColor: self.fillColor, accentBackgroundColor: self.fillColor, defaultBorderColor: self.strokeColor, mediaBorderColor: self.strokeColor, chatBorderColor: self.strokeColor, check: self.foregroundColor, blueColor: self.fillColor, barBackgroundColor: self.fillColor))!
|
let checkView = TGCheckButtonView(style: style, pallete: TGCheckButtonPallete(defaultBackgroundColor: self.fillColor, accentBackgroundColor: self.fillColor, defaultBorderColor: self.strokeColor, mediaBorderColor: self.strokeColor, chatBorderColor: self.strokeColor, check: self.foregroundColor, blueColor: self.fillColor, barBackgroundColor: self.fillColor))!
|
||||||
checkView.setSelected(true, animated: false)
|
checkView.setSelected(true, animated: false)
|
||||||
|
@ -8,7 +8,8 @@ typedef enum
|
|||||||
TGCheckButtonStyleMedia,
|
TGCheckButtonStyleMedia,
|
||||||
TGCheckButtonStyleGallery,
|
TGCheckButtonStyleGallery,
|
||||||
TGCheckButtonStyleShare,
|
TGCheckButtonStyleShare,
|
||||||
TGCheckButtonStyleChat
|
TGCheckButtonStyleChat,
|
||||||
|
TGCheckButtonStyleCompact
|
||||||
} TGCheckButtonStyle;
|
} TGCheckButtonStyle;
|
||||||
|
|
||||||
@interface TGCheckButtonPallete : NSObject
|
@interface TGCheckButtonPallete : NSObject
|
||||||
|
@ -106,6 +106,12 @@
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case TGCheckButtonStyleCompact:
|
||||||
|
{
|
||||||
|
insideInset = 6.0f;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
insideInset = 5.0f;
|
insideInset = 5.0f;
|
||||||
@ -182,13 +188,18 @@
|
|||||||
|
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
|
CGFloat lineWidth = 1.0f;
|
||||||
|
if (style == TGCheckButtonStyleCompact) {
|
||||||
|
lineWidth = 1.5f;
|
||||||
|
}
|
||||||
|
|
||||||
CGRect rect = CGRectMake(0, 0, size.width, size.height);
|
CGRect rect = CGRectMake(0, 0, size.width, size.height);
|
||||||
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0);
|
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0);
|
||||||
CGContextRef context = UIGraphicsGetCurrentContext();
|
CGContextRef context = UIGraphicsGetCurrentContext();
|
||||||
CGContextSetLineWidth(context, 1.0f);
|
CGContextSetLineWidth(context, lineWidth);
|
||||||
|
|
||||||
CGContextSetStrokeColorWithColor(context, borderColor.CGColor);
|
CGContextSetStrokeColorWithColor(context, borderColor.CGColor);
|
||||||
CGContextStrokeEllipseInRect(context, CGRectInset(rect, insideInset + 0.5f, insideInset + 0.5f));
|
CGContextStrokeEllipseInRect(context, CGRectInset(rect, insideInset + lineWidth / 2.0, insideInset + lineWidth / 2.0));
|
||||||
|
|
||||||
backgroundImage = UIGraphicsGetImageFromCurrentImageContext();
|
backgroundImage = UIGraphicsGetImageFromCurrentImageContext();
|
||||||
UIGraphicsEndImageContext();
|
UIGraphicsEndImageContext();
|
||||||
@ -234,7 +245,7 @@
|
|||||||
|
|
||||||
UIColor *color = style == TGCheckButtonStyleDefaultBlue ? blueColor : greenColor;
|
UIColor *color = style == TGCheckButtonStyleDefaultBlue ? blueColor : greenColor;
|
||||||
CGContextSetFillColorWithColor(context, color.CGColor);
|
CGContextSetFillColorWithColor(context, color.CGColor);
|
||||||
CGFloat inset = (style == TGCheckButtonStyleDefault || style == TGCheckButtonStyleDefaultBlue) ? 0.0f : 1.2f;
|
CGFloat inset = (style == TGCheckButtonStyleDefault || style == TGCheckButtonStyleDefaultBlue || style == TGCheckButtonStyleCompact) ? 0.0f : 1.2f;
|
||||||
CGContextFillEllipseInRect(context, CGRectInset(rect, insideInset + inset, insideInset + inset));
|
CGContextFillEllipseInRect(context, CGRectInset(rect, insideInset + inset, insideInset + inset));
|
||||||
|
|
||||||
fillImage = UIGraphicsGetImageFromCurrentImageContext();
|
fillImage = UIGraphicsGetImageFromCurrentImageContext();
|
||||||
|
@ -454,13 +454,15 @@ private final class SemanticStatusNodeDrawingState: NSObject {
|
|||||||
let hollow: Bool
|
let hollow: Bool
|
||||||
let transitionState: SemanticStatusNodeTransitionDrawingState?
|
let transitionState: SemanticStatusNodeTransitionDrawingState?
|
||||||
let drawingState: SemanticStatusNodeStateDrawingState
|
let drawingState: SemanticStatusNodeStateDrawingState
|
||||||
|
let cutout: SemanticStatusNode.Cutout?
|
||||||
|
|
||||||
init(background: UIColor, foreground: UIColor, hollow: Bool, transitionState: SemanticStatusNodeTransitionDrawingState?, drawingState: SemanticStatusNodeStateDrawingState) {
|
init(background: UIColor, foreground: UIColor, hollow: Bool, transitionState: SemanticStatusNodeTransitionDrawingState?, drawingState: SemanticStatusNodeStateDrawingState, cutout: SemanticStatusNode.Cutout?) {
|
||||||
self.background = background
|
self.background = background
|
||||||
self.foreground = foreground
|
self.foreground = foreground
|
||||||
self.hollow = hollow
|
self.hollow = hollow
|
||||||
self.transitionState = transitionState
|
self.transitionState = transitionState
|
||||||
self.drawingState = drawingState
|
self.drawingState = drawingState
|
||||||
|
self.cutout = cutout
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
@ -481,6 +483,10 @@ private final class SemanticStatusNodeTransitionContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public final class SemanticStatusNode: ASControlNode {
|
public final class SemanticStatusNode: ASControlNode {
|
||||||
|
final class Cutout {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public var backgroundNodeColor: UIColor {
|
public var backgroundNodeColor: UIColor {
|
||||||
didSet {
|
didSet {
|
||||||
if !self.backgroundNodeColor.isEqual(oldValue) {
|
if !self.backgroundNodeColor.isEqual(oldValue) {
|
||||||
@ -589,7 +595,7 @@ public final class SemanticStatusNode: ASControlNode {
|
|||||||
transitionState = SemanticStatusNodeTransitionDrawingState(transition: t, drawingState: transitionContext.previousStateContext.drawingState(transitionFraction: 1.0 - t))
|
transitionState = SemanticStatusNodeTransitionDrawingState(transition: t, drawingState: transitionContext.previousStateContext.drawingState(transitionFraction: 1.0 - t))
|
||||||
}
|
}
|
||||||
|
|
||||||
return SemanticStatusNodeDrawingState(background: self.backgroundNodeColor, foreground: self.foregroundNodeColor, hollow: self.hollow, transitionState: transitionState, drawingState: self.stateContext.drawingState(transitionFraction: transitionFraction))
|
return SemanticStatusNodeDrawingState(background: self.backgroundNodeColor, foreground: self.foregroundNodeColor, hollow: self.hollow, transitionState: transitionState, drawingState: self.stateContext.drawingState(transitionFraction: transitionFraction), cutout: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
|
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
|
||||||
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -6967,6 +6967,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
|
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
|
||||||
|
|
||||||
|
var groupingKey: Int64?
|
||||||
|
var allItemsAreAudio = true
|
||||||
|
for item in results {
|
||||||
|
if let item = item {
|
||||||
|
let pathExtension = (item.fileName as NSString).pathExtension.lowercased()
|
||||||
|
if !["mp3", "m4a"].contains(pathExtension) {
|
||||||
|
allItemsAreAudio = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if allItemsAreAudio {
|
||||||
|
groupingKey = arc4random64()
|
||||||
|
}
|
||||||
|
|
||||||
var messages: [EnqueueMessage] = []
|
var messages: [EnqueueMessage] = []
|
||||||
for item in results {
|
for item in results {
|
||||||
if let item = item {
|
if let item = item {
|
||||||
@ -6977,7 +6992,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 320, height: 320), resource: ICloudFileResource(urlData: item.urlData, thumbnail: true), progressiveSizes: []))
|
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 320, height: 320), resource: ICloudFileResource(urlData: item.urlData, thumbnail: true), progressiveSizes: []))
|
||||||
}
|
}
|
||||||
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData, thumbnail: false), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: item.fileSize, attributes: [.FileName(fileName: item.fileName)])
|
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData, thumbnail: false), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: item.fileSize, attributes: [.FileName(fileName: item.fileName)])
|
||||||
let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: replyMessageId, localGroupingKey: nil)
|
let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: replyMessageId, localGroupingKey: groupingKey)
|
||||||
messages.append(message)
|
messages.append(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -6985,7 +7000,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
if !messages.isEmpty {
|
if !messages.isEmpty {
|
||||||
if editingMessage {
|
if editingMessage {
|
||||||
strongSelf.editMessageMediaWithMessages(messages)
|
strongSelf.editMessageMediaWithMessages(messages)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
|
@ -474,7 +474,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let (_, refineLayout) = contentFileLayout(context, presentationData, message, chatLocation, attributes, file, automaticDownload, message.effectivelyIncoming(context.account.peerId), false, associatedData.forcedResourceStatus, statusType, CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height))
|
let (_, refineLayout) = contentFileLayout(context, presentationData, message, chatLocation, attributes, file, automaticDownload, message.effectivelyIncoming(context.account.peerId), false, associatedData.forcedResourceStatus, statusType, nil, CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height))
|
||||||
refineContentFileLayout = refineLayout
|
refineContentFileLayout = refineLayout
|
||||||
}
|
}
|
||||||
} else if let image = media as? TelegramMediaImage {
|
} else if let image = media as? TelegramMediaImage {
|
||||||
|
@ -1148,7 +1148,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
|
|||||||
let contentItem = ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: message, read: read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: attributes)
|
let contentItem = ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: message, read: read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: attributes)
|
||||||
|
|
||||||
var itemSelection: Bool?
|
var itemSelection: Bool?
|
||||||
if case .mosaic = prepareContentPosition {
|
|
||||||
switch content {
|
switch content {
|
||||||
case .message:
|
case .message:
|
||||||
break
|
break
|
||||||
@ -1165,7 +1164,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let (properties, unboundSize, maxNodeWidth, nodeLayout) = prepareLayout(contentItem, layoutConstants, prepareContentPosition, itemSelection, CGSize(width: maximumContentWidth, height: CGFloat.greatestFiniteMagnitude))
|
let (properties, unboundSize, maxNodeWidth, nodeLayout) = prepareLayout(contentItem, layoutConstants, prepareContentPosition, itemSelection, CGSize(width: maximumContentWidth, height: CGFloat.greatestFiniteMagnitude))
|
||||||
maximumNodeWidth = min(maximumNodeWidth, maxNodeWidth)
|
maximumNodeWidth = min(maximumNodeWidth, maxNodeWidth)
|
||||||
@ -2777,7 +2775,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func traceSelectionNodes(parent: ASDisplayNode, point: CGPoint) -> ASDisplayNode? {
|
private func traceSelectionNodes(parent: ASDisplayNode, point: CGPoint) -> ASDisplayNode? {
|
||||||
if let parent = parent as? GridMessageSelectionNode, parent.bounds.contains(point) {
|
if let parent = parent as? FileMessageSelectionNode, parent.bounds.contains(point) {
|
||||||
|
return parent
|
||||||
|
} else if let parent = parent as? GridMessageSelectionNode, parent.bounds.contains(point) {
|
||||||
return parent
|
return parent
|
||||||
} else {
|
} else {
|
||||||
if let parentSubnodes = parent.subnodes {
|
if let parentSubnodes = parent.subnodes {
|
||||||
|
@ -34,22 +34,24 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
|
|
||||||
self.addSubnode(self.interactiveFileNode)
|
self.addSubnode(self.interactiveFileNode)
|
||||||
|
|
||||||
self.interactiveFileNode.activateLocalContent = { [weak self] in
|
self.interactiveFileNode.toggleSelection = { [weak self] value in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self, let item = strongSelf.item {
|
||||||
if let item = strongSelf.item {
|
item.controllerInteraction.toggleMessagesSelection([item.message.id], value)
|
||||||
let _ = item.controllerInteraction.openMessage(item.message, .default)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.interactiveFileNode.activateLocalContent = { [weak self] in
|
||||||
|
if let strongSelf = self, let item = strongSelf.item {
|
||||||
|
let _ = item.controllerInteraction.openMessage(item.message, .default)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.interactiveFileNode.requestUpdateLayout = { [weak self] _ in
|
self.interactiveFileNode.requestUpdateLayout = { [weak self] _ in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self, let item = strongSelf.item {
|
||||||
if let item = strongSelf.item {
|
|
||||||
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id)
|
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder) {
|
required init?(coder aDecoder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
@ -58,7 +60,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
|
override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
|
||||||
let interactiveFileLayout = self.interactiveFileNode.asyncLayout()
|
let interactiveFileLayout = self.interactiveFileNode.asyncLayout()
|
||||||
|
|
||||||
return { item, layoutConstants, preparePosition, _, constrainedSize in
|
return { item, layoutConstants, preparePosition, selection, constrainedSize in
|
||||||
var selectedFile: TelegramMediaFile?
|
var selectedFile: TelegramMediaFile?
|
||||||
for media in item.message.media {
|
for media in item.message.media {
|
||||||
if let telegramFile = media as? TelegramMediaFile {
|
if let telegramFile = media as? TelegramMediaFile {
|
||||||
@ -87,7 +89,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
|
|
||||||
let automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: selectedFile!)
|
let automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: selectedFile!)
|
||||||
|
|
||||||
let (initialWidth, refineLayout) = interactiveFileLayout(item.context, item.presentationData, item.message, item.chatLocation, item.attributes, selectedFile!, automaticDownload, item.message.effectivelyIncoming(item.context.account.peerId), item.associatedData.isRecentActions, item.associatedData.forcedResourceStatus, statusType, CGSize(width: constrainedSize.width - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right, height: constrainedSize.height))
|
let (initialWidth, refineLayout) = interactiveFileLayout(item.context, item.presentationData, item.message, item.chatLocation, item.attributes, selectedFile!, automaticDownload, item.message.effectivelyIncoming(item.context.account.peerId), item.associatedData.isRecentActions, item.associatedData.forcedResourceStatus, statusType, item.message.groupingKey != nil ? selection : nil, CGSize(width: constrainedSize.width - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right, height: constrainedSize.height))
|
||||||
|
|
||||||
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
|
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
|
||||||
|
|
||||||
@ -97,7 +99,15 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
return (refinedWidth + layoutConstants.file.bubbleInsets.left + layoutConstants.file.bubbleInsets.right, { boundingWidth in
|
return (refinedWidth + layoutConstants.file.bubbleInsets.left + layoutConstants.file.bubbleInsets.right, { boundingWidth in
|
||||||
let (fileSize, fileApply) = finishLayout(boundingWidth - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right)
|
let (fileSize, fileApply) = finishLayout(boundingWidth - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right)
|
||||||
|
|
||||||
return (CGSize(width: fileSize.width + layoutConstants.file.bubbleInsets.left + layoutConstants.file.bubbleInsets.right, height: fileSize.height + layoutConstants.file.bubbleInsets.top + layoutConstants.file.bubbleInsets.bottom), { [weak self] _, synchronousLoads in
|
var bottomInset = layoutConstants.file.bubbleInsets.bottom
|
||||||
|
|
||||||
|
if case let .linear(_, bottom) = position {
|
||||||
|
if case .Neighbour = bottom {
|
||||||
|
bottomInset -= 20.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (CGSize(width: fileSize.width + layoutConstants.file.bubbleInsets.left + layoutConstants.file.bubbleInsets.right, height: fileSize.height + layoutConstants.file.bubbleInsets.top + bottomInset), { [weak self] _, synchronousLoads in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.item = item
|
strongSelf.item = item
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ import TelegramStringFormatting
|
|||||||
import RadialStatusNode
|
import RadialStatusNode
|
||||||
import SemanticStatusNode
|
import SemanticStatusNode
|
||||||
import FileMediaResourceStatus
|
import FileMediaResourceStatus
|
||||||
|
import CheckNode
|
||||||
|
|
||||||
private struct FetchControls {
|
private struct FetchControls {
|
||||||
let fetch: () -> Void
|
let fetch: () -> Void
|
||||||
@ -21,6 +22,10 @@ private struct FetchControls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
||||||
|
private var selectionBackgroundNode: ASDisplayNode?
|
||||||
|
private var selectionNode: FileMessageSelectionNode?
|
||||||
|
private var cutoutNode: ASDisplayNode?
|
||||||
|
|
||||||
private let titleNode: TextNode
|
private let titleNode: TextNode
|
||||||
private let descriptionNode: TextNode
|
private let descriptionNode: TextNode
|
||||||
private let descriptionMeasuringNode: TextNode
|
private let descriptionMeasuringNode: TextNode
|
||||||
@ -35,7 +40,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
private var iconNode: TransformImageNode?
|
private var iconNode: TransformImageNode?
|
||||||
private var statusNode: SemanticStatusNode?
|
private var statusNode: SemanticStatusNode?
|
||||||
private var playbackAudioLevelView: VoiceBlobView?
|
private var playbackAudioLevelView: VoiceBlobView?
|
||||||
private var streamingStatusNode: RadialStatusNode?
|
private var streamingStatusNode: SemanticStatusNode?
|
||||||
private var tapRecognizer: UITapGestureRecognizer?
|
private var tapRecognizer: UITapGestureRecognizer?
|
||||||
|
|
||||||
private let statusDisposable = MetaDisposable()
|
private let statusDisposable = MetaDisposable()
|
||||||
@ -76,6 +81,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
private var actualFetchStatus: MediaResourceStatus?
|
private var actualFetchStatus: MediaResourceStatus?
|
||||||
private let fetchDisposable = MetaDisposable()
|
private let fetchDisposable = MetaDisposable()
|
||||||
|
|
||||||
|
var toggleSelection: (Bool) -> Void = { _ in }
|
||||||
var activateLocalContent: () -> Void = { }
|
var activateLocalContent: () -> Void = { }
|
||||||
var requestUpdateLayout: (Bool) -> Void = { _ in }
|
var requestUpdateLayout: (Bool) -> Void = { _ in }
|
||||||
|
|
||||||
@ -86,8 +92,6 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
private var progressFrame: CGRect?
|
private var progressFrame: CGRect?
|
||||||
private var streamingCacheStatusFrame: CGRect?
|
private var streamingCacheStatusFrame: CGRect?
|
||||||
private var fileIconImage: UIImage?
|
private var fileIconImage: UIImage?
|
||||||
private var cloudFetchIconImage: UIImage?
|
|
||||||
private var cloudFetchedIconImage: UIImage?
|
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
self.titleNode = TextNode()
|
self.titleNode = TextNode()
|
||||||
@ -204,7 +208,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))) {
|
func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))) {
|
||||||
let currentFile = self.file
|
let currentFile = self.file
|
||||||
|
|
||||||
let titleAsyncLayout = TextNode.asyncLayout(self.titleNode)
|
let titleAsyncLayout = TextNode.asyncLayout(self.titleNode)
|
||||||
@ -214,7 +218,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
|
|
||||||
let currentMessage = self.message
|
let currentMessage = self.message
|
||||||
|
|
||||||
return { context, presentationData, message, chatLocation, attributes, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, constrainedSize in
|
return { context, presentationData, message, chatLocation, attributes, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, messageSelection, constrainedSize in
|
||||||
return (CGFloat.greatestFiniteMagnitude, { constrainedSize in
|
return (CGFloat.greatestFiniteMagnitude, { constrainedSize in
|
||||||
let titleFont = Font.regular(floor(presentationData.fontSize.baseDisplaySize * 16.0 / 17.0))
|
let titleFont = Font.regular(floor(presentationData.fontSize.baseDisplaySize * 16.0 / 17.0))
|
||||||
let descriptionFont = Font.regular(floor(presentationData.fontSize.baseDisplaySize * 13.0 / 17.0))
|
let descriptionFont = Font.regular(floor(presentationData.fontSize.baseDisplaySize * 13.0 / 17.0))
|
||||||
@ -415,15 +419,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
textConstrainedSize.width -= 80.0
|
textConstrainedSize.width -= 80.0
|
||||||
}
|
}
|
||||||
|
|
||||||
let streamingProgressDiameter: CGFloat = 28.0
|
let streamingProgressDiameter: CGFloat = 20.0
|
||||||
var hasStreamingProgress = false
|
|
||||||
if isAudio && !isVoice {
|
|
||||||
hasStreamingProgress = true
|
|
||||||
|
|
||||||
if hasStreamingProgress {
|
|
||||||
textConstrainedSize.width -= streamingProgressDiameter + 4.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let (titleLayout, titleApply) = titleAsyncLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: hasThumbnail ? 2 : 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
let (titleLayout, titleApply) = titleAsyncLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: hasThumbnail ? 2 : 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
let (descriptionLayout, descriptionApply) = descriptionAsyncLayout(TextNodeLayoutArguments(attributedString: descriptionString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
let (descriptionLayout, descriptionApply) = descriptionAsyncLayout(TextNodeLayoutArguments(attributedString: descriptionString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
@ -462,14 +458,6 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
minLayoutWidth = max(minLayoutWidth, statusSize.width)
|
minLayoutWidth = max(minLayoutWidth, statusSize.width)
|
||||||
}
|
}
|
||||||
|
|
||||||
var cloudFetchIconImage: UIImage?
|
|
||||||
var cloudFetchedIconImage: UIImage?
|
|
||||||
if hasStreamingProgress {
|
|
||||||
minLayoutWidth += streamingProgressDiameter + 4.0
|
|
||||||
cloudFetchIconImage = incoming ? PresentationResourcesChat.chatBubbleFileCloudFetchIncomingIcon(presentationData.theme.theme) : PresentationResourcesChat.chatBubbleFileCloudFetchOutgoingIcon(presentationData.theme.theme)
|
|
||||||
cloudFetchedIconImage = incoming ? PresentationResourcesChat.chatBubbleFileCloudFetchedIncomingIcon(presentationData.theme.theme) : PresentationResourcesChat.chatBubbleFileCloudFetchedOutgoingIcon(presentationData.theme.theme)
|
|
||||||
}
|
|
||||||
|
|
||||||
let fileIconImage: UIImage?
|
let fileIconImage: UIImage?
|
||||||
if hasThumbnail {
|
if hasThumbnail {
|
||||||
fileIconImage = nil
|
fileIconImage = nil
|
||||||
@ -551,10 +539,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if isAudio && !isVoice {
|
if isAudio && !isVoice {
|
||||||
streamingCacheStatusFrame = CGRect(origin: CGPoint(x: boundingWidth - streamingProgressDiameter + 1.0, y: 8.0), size: CGSize(width: streamingProgressDiameter, height: streamingProgressDiameter))
|
streamingCacheStatusFrame = CGRect(origin: CGPoint(x: progressFrame.maxX - streamingProgressDiameter + 2.0, y: progressFrame.maxY - streamingProgressDiameter + 2.0), size: CGSize(width: streamingProgressDiameter, height: streamingProgressDiameter))
|
||||||
if hasStreamingProgress {
|
|
||||||
fittedLayoutSize.width += streamingProgressDiameter + 6.0
|
|
||||||
}
|
|
||||||
fittedLayoutSize.width = max(fittedLayoutSize.width, boundingWidth + 2.0)
|
fittedLayoutSize.width = max(fittedLayoutSize.width, boundingWidth + 2.0)
|
||||||
} else {
|
} else {
|
||||||
streamingCacheStatusFrame = CGRect()
|
streamingCacheStatusFrame = CGRect()
|
||||||
@ -696,8 +681,6 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
strongSelf.progressFrame = progressFrame
|
strongSelf.progressFrame = progressFrame
|
||||||
strongSelf.streamingCacheStatusFrame = streamingCacheStatusFrame
|
strongSelf.streamingCacheStatusFrame = streamingCacheStatusFrame
|
||||||
strongSelf.fileIconImage = fileIconImage
|
strongSelf.fileIconImage = fileIconImage
|
||||||
strongSelf.cloudFetchIconImage = cloudFetchIconImage
|
|
||||||
strongSelf.cloudFetchedIconImage = cloudFetchedIconImage
|
|
||||||
|
|
||||||
if let updatedFetchControls = updatedFetchControls {
|
if let updatedFetchControls = updatedFetchControls {
|
||||||
let _ = strongSelf.fetchControls.swap(updatedFetchControls)
|
let _ = strongSelf.fetchControls.swap(updatedFetchControls)
|
||||||
@ -706,7 +689,64 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
strongSelf.updateStatus(animated: !synchronousLoads)
|
let isAnimated = !synchronousLoads
|
||||||
|
let transition: ContainedViewLayoutTransition = isAnimated ? .animated(duration: 0.2, curve: .spring) : .immediate
|
||||||
|
if let selection = messageSelection {
|
||||||
|
if let streamingStatusNode = strongSelf.streamingStatusNode {
|
||||||
|
transition.updateAlpha(node: streamingStatusNode, alpha: 0.0)
|
||||||
|
transition.updateTransformScale(node: streamingStatusNode, scale: 0.2)
|
||||||
|
}
|
||||||
|
let selectionFrame = CGRect(origin: CGPoint(), size: fittedLayoutSize)
|
||||||
|
if let selectionNode = strongSelf.selectionNode {
|
||||||
|
selectionNode.frame = selectionFrame
|
||||||
|
selectionNode.updateSelected(selection, animated: isAnimated)
|
||||||
|
} else {
|
||||||
|
let selectionNode = FileMessageSelectionNode(theme: presentationData.theme.theme, incoming: incoming, toggle: { [weak self] value in
|
||||||
|
self?.toggleSelection(value)
|
||||||
|
})
|
||||||
|
strongSelf.selectionNode = selectionNode
|
||||||
|
strongSelf.addSubnode(selectionNode)
|
||||||
|
selectionNode.frame = selectionFrame
|
||||||
|
selectionNode.updateSelected(selection, animated: false)
|
||||||
|
if isAnimated {
|
||||||
|
selectionNode.animateIn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectionBackgroundFrame = CGRect(origin: CGPoint(x: -8.0, y: -9.0), size: CGSize(width: fittedLayoutSize.width + 16.0, height: fittedLayoutSize.height + 6.0))
|
||||||
|
if let selectionBackgroundNode = strongSelf.selectionBackgroundNode {
|
||||||
|
selectionBackgroundNode.frame = selectionBackgroundFrame
|
||||||
|
selectionBackgroundNode.isHidden = !selection
|
||||||
|
} else {
|
||||||
|
let selectionBackgroundNode = ASDisplayNode()
|
||||||
|
selectionBackgroundNode.backgroundColor = messageTheme.accentControlColor.withAlphaComponent(0.08)
|
||||||
|
selectionBackgroundNode.frame = selectionBackgroundFrame
|
||||||
|
selectionBackgroundNode.isHidden = !selection
|
||||||
|
strongSelf.selectionBackgroundNode = selectionBackgroundNode
|
||||||
|
strongSelf.insertSubnode(selectionBackgroundNode, at: 0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let streamingStatusNode = strongSelf.streamingStatusNode {
|
||||||
|
transition.updateAlpha(node: streamingStatusNode, alpha: 1.0)
|
||||||
|
transition.updateTransformScale(node: streamingStatusNode, scale: 1.0)
|
||||||
|
}
|
||||||
|
if let selectionNode = strongSelf.selectionNode {
|
||||||
|
strongSelf.selectionNode = nil
|
||||||
|
if isAnimated {
|
||||||
|
selectionNode.animateOut(completion: { [weak selectionNode] in
|
||||||
|
selectionNode?.removeFromSupernode()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
selectionNode.removeFromSupernode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let selectionBackgroundNode = strongSelf.selectionBackgroundNode {
|
||||||
|
strongSelf.selectionBackgroundNode = nil
|
||||||
|
selectionBackgroundNode.removeFromSupernode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
strongSelf.updateStatus(animated: isAnimated)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -751,7 +791,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let state: SemanticStatusNodeState
|
let state: SemanticStatusNodeState
|
||||||
var streamingState: RadialStatusNodeState = .none
|
var streamingState: SemanticStatusNodeState = .none
|
||||||
|
|
||||||
let isSending = message.flags.isSending
|
let isSending = message.flags.isSending
|
||||||
|
|
||||||
@ -795,35 +835,19 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if isAudio && !isVoice && !isSending {
|
if isAudio && !isVoice && !isSending {
|
||||||
let streamingStatusForegroundColor: UIColor = messageTheme.accentControlColor
|
|
||||||
let streamingStatusBackgroundColor: UIColor = messageTheme.mediaInactiveControlColor
|
|
||||||
switch resourceStatus.fetchStatus {
|
switch resourceStatus.fetchStatus {
|
||||||
case let .Fetching(_, progress):
|
case let .Fetching(_, progress):
|
||||||
let adjustedProgress = max(progress, 0.027)
|
let adjustedProgress = max(progress, 0.027)
|
||||||
streamingState = .cloudProgress(color: streamingStatusForegroundColor, strokeBackgroundColor: streamingStatusBackgroundColor, lineWidth: 2.0, value: CGFloat(adjustedProgress))
|
streamingState = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: .init(inset: 1.0, lineWidth: 2.0))
|
||||||
case .Local:
|
case .Local:
|
||||||
if let cloudFetchedIconImage = self.cloudFetchedIconImage {
|
|
||||||
streamingState = .customIcon(cloudFetchedIconImage)
|
|
||||||
} else {
|
|
||||||
streamingState = .none
|
streamingState = .none
|
||||||
}
|
|
||||||
case .Remote:
|
case .Remote:
|
||||||
if let cloudFetchIconImage = self.cloudFetchIconImage {
|
streamingState = .download
|
||||||
streamingState = .customIcon(cloudFetchIconImage)
|
|
||||||
} else {
|
|
||||||
streamingState = .none
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
streamingState = .none
|
streamingState = .none
|
||||||
}
|
}
|
||||||
|
|
||||||
let statusForegroundColor: UIColor
|
|
||||||
if self.iconNode != nil {
|
|
||||||
statusForegroundColor = presentationData.theme.theme.chat.message.mediaOverlayControlColors.foregroundColor
|
|
||||||
} else {
|
|
||||||
statusForegroundColor = incoming ? presentationData.theme.theme.chat.message.incoming.mediaControlInnerBackgroundColor : presentationData.theme.theme.chat.message.outgoing.mediaControlInnerBackgroundColor
|
|
||||||
}
|
|
||||||
switch resourceStatus.mediaStatus {
|
switch resourceStatus.mediaStatus {
|
||||||
case var .fetchStatus(fetchStatus):
|
case var .fetchStatus(fetchStatus):
|
||||||
if self.message?.forwardInfo != nil {
|
if self.message?.forwardInfo != nil {
|
||||||
@ -903,7 +927,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
self.playbackAudioLevelView?.setColor(presentationData.theme.theme.chat.inputPanel.actionControlFillColor)
|
self.playbackAudioLevelView?.setColor(presentationData.theme.theme.chat.inputPanel.actionControlFillColor)
|
||||||
|
|
||||||
if streamingState != .none && self.streamingStatusNode == nil {
|
if streamingState != .none && self.streamingStatusNode == nil {
|
||||||
let streamingStatusNode = RadialStatusNode(backgroundNodeColor: .clear)
|
let streamingStatusNode = SemanticStatusNode(backgroundNodeColor: backgroundNodeColor, foregroundNodeColor: foregroundNodeColor)
|
||||||
self.streamingStatusNode = streamingStatusNode
|
self.streamingStatusNode = streamingStatusNode
|
||||||
streamingStatusNode.frame = streamingCacheStatusFrame
|
streamingStatusNode.frame = streamingCacheStatusFrame
|
||||||
self.addSubnode(streamingStatusNode)
|
self.addSubnode(streamingStatusNode)
|
||||||
@ -944,6 +968,36 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if streamingState == .none && self.selectionNode == nil {
|
||||||
|
if let cutoutNode = self.cutoutNode {
|
||||||
|
self.cutoutNode = nil
|
||||||
|
if animated {
|
||||||
|
cutoutNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) { [weak cutoutNode] _ in
|
||||||
|
cutoutNode?.removeFromSupernode()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cutoutNode.removeFromSupernode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let statusNode = self.statusNode {
|
||||||
|
if let _ = self.cutoutNode {
|
||||||
|
} else {
|
||||||
|
let cutoutNode = ASImageNode()
|
||||||
|
cutoutNode.displaysAsynchronously = false
|
||||||
|
cutoutNode.displayWithoutProcessing = true
|
||||||
|
cutoutNode.image = generateFilledCircleImage(diameter: 23.0, color: messageTheme.bubble.withWallpaper.fill)
|
||||||
|
|
||||||
|
self.cutoutNode = cutoutNode
|
||||||
|
self.insertSubnode(cutoutNode, aboveSubnode: statusNode)
|
||||||
|
|
||||||
|
cutoutNode.frame = streamingCacheStatusFrame.insetBy(dx: -1.5, dy: -1.5)
|
||||||
|
|
||||||
|
if animated {
|
||||||
|
cutoutNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let (expandedString, compactString, font) = downloadingStrings {
|
if let (expandedString, compactString, font) = downloadingStrings {
|
||||||
self.fetchingTextNode.attributedText = NSAttributedString(string: expandedString, font: font, textColor: messageTheme.fileDurationColor)
|
self.fetchingTextNode.attributedText = NSAttributedString(string: expandedString, font: font, textColor: messageTheme.fileDurationColor)
|
||||||
self.fetchingCompactTextNode.attributedText = NSAttributedString(string: compactString, font: font, textColor: messageTheme.fileDurationColor)
|
self.fetchingCompactTextNode.attributedText = NSAttributedString(string: compactString, font: font, textColor: messageTheme.fileDurationColor)
|
||||||
@ -975,12 +1029,12 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
self.fetchingCompactTextNode.frame = CGRect(origin: self.descriptionNode.frame.origin, size: fetchingCompactSize)
|
self.fetchingCompactTextNode.frame = CGRect(origin: self.descriptionNode.frame.origin, size: fetchingCompactSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> ChatMessageInteractiveFileNode))) {
|
static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> ChatMessageInteractiveFileNode))) {
|
||||||
let currentAsyncLayout = node?.asyncLayout()
|
let currentAsyncLayout = node?.asyncLayout()
|
||||||
|
|
||||||
return { context, presentationData, message, chatLocation, attributes, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, constrainedSize in
|
return { context, presentationData, message, chatLocation, attributes, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, messageSelection, constrainedSize in
|
||||||
var fileNode: ChatMessageInteractiveFileNode
|
var fileNode: ChatMessageInteractiveFileNode
|
||||||
var fileLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void)))
|
var fileLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void)))
|
||||||
|
|
||||||
if let node = node, let currentAsyncLayout = currentAsyncLayout {
|
if let node = node, let currentAsyncLayout = currentAsyncLayout {
|
||||||
fileNode = node
|
fileNode = node
|
||||||
@ -990,7 +1044,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
fileLayout = fileNode.asyncLayout()
|
fileLayout = fileNode.asyncLayout()
|
||||||
}
|
}
|
||||||
|
|
||||||
let (initialWidth, continueLayout) = fileLayout(context, presentationData, message, chatLocation, attributes, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, constrainedSize)
|
let (initialWidth, continueLayout) = fileLayout(context, presentationData, message, chatLocation, attributes, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, messageSelection, constrainedSize)
|
||||||
|
|
||||||
return (initialWidth, { constrainedSize in
|
return (initialWidth, { constrainedSize in
|
||||||
let (finalWidth, finalLayout) = continueLayout(constrainedSize)
|
let (finalWidth, finalLayout) = continueLayout(constrainedSize)
|
||||||
@ -1053,3 +1107,60 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
final class FileMessageSelectionNode: ASDisplayNode {
|
||||||
|
private let toggle: (Bool) -> Void
|
||||||
|
|
||||||
|
private var selected = false
|
||||||
|
private let checkNode: CheckNode
|
||||||
|
|
||||||
|
public init(theme: PresentationTheme, incoming: Bool, toggle: @escaping (Bool) -> Void) {
|
||||||
|
self.toggle = toggle
|
||||||
|
self.checkNode = CheckNode(strokeColor: incoming ? theme.chat.message.incoming.mediaPlaceholderColor : theme.chat.message.outgoing.mediaPlaceholderColor, fillColor: theme.list.itemCheckColors.fillColor, foregroundColor: theme.list.itemCheckColors.foregroundColor, style: .compact)
|
||||||
|
self.checkNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.addSubnode(self.checkNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func didLoad() {
|
||||||
|
super.didLoad()
|
||||||
|
|
||||||
|
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||||
|
}
|
||||||
|
|
||||||
|
public func animateIn() {
|
||||||
|
self.checkNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
||||||
|
self.checkNode.layer.animateScale(from: 0.2, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func animateOut(completion: @escaping () -> Void) {
|
||||||
|
self.checkNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||||
|
self.checkNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
|
||||||
|
completion()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public func updateSelected(_ selected: Bool, animated: Bool) {
|
||||||
|
if self.selected != selected {
|
||||||
|
self.selected = selected
|
||||||
|
self.checkNode.setIsChecked(selected, animated: animated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||||
|
if case .ended = recognizer.state {
|
||||||
|
self.toggle(!self.selected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func layout() {
|
||||||
|
super.layout()
|
||||||
|
|
||||||
|
let checkSize = CGSize(width: 30.0, height: 30.0)
|
||||||
|
self.checkNode.frame = CGRect(origin: CGPoint(x: 23.0, y: 17.0), size: checkSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user