Grouped music files

This commit is contained in:
Ilya Laktyushin 2020-10-06 21:57:44 +04:00
parent 0da14b56dd
commit 814618bcf4
14 changed files with 3071 additions and 2895 deletions

View File

@ -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.";
"Conversation.ContextViewStats" = "View Statistics";
"ChatList.MessageMusic_1" = "1 Music File";
"ChatList.MessageMusic_any" = "%@ Music Files";

View File

@ -104,13 +104,13 @@ func suggestDates(for string: String, strings: PresentationStrings, dateTimeForm
let stringComponents = string.components(separatedBy: dateSeparator)
if stringComponents.count < 3 {
for i in 0..<5 {
if let date = calendar.date(byAdding: .year, value: -i, to: resultDate), date < now {
for i in 0..<8 {
if let date = calendar.date(byAdding: .year, value: -i, to: resultDate), date < now, date > telegramReleaseDate {
let lowerDate = getLowerDate(for: resultDate)
result.append((lowerDate, date, nil))
}
}
} else if resultDate < now {
} else if resultDate < now, date > telegramReleaseDate {
let lowerDate = getLowerDate(for: resultDate)
result.append((lowerDate, resultDate, nil))
}

View File

@ -10,6 +10,7 @@ import LocalizedPeerData
private enum MessageGroupType {
case photos
case videos
case music
case generic
}
@ -18,6 +19,9 @@ private func singleMessageType(message: Message) -> MessageGroupType {
if let _ = media as? TelegramMediaImage {
return .photos
} else if let file = media as? TelegramMediaFile {
if file.isMusic {
return .music
}
if file.isVideo && !file.isInstantVideo {
return .videos
}
@ -80,6 +84,13 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
messageText = strings.ChatList_MessageVideos(Int32(messages.count))
textIsReady = true
}
case .music:
if !messageText.isEmpty {
textIsReady = true
} else {
messageText = strings.ChatList_MessageMusic(Int32(messages.count))
textIsReady = true
}
case .generic:
break
}

View File

@ -8,6 +8,7 @@ public enum CheckNodeStyle {
case plain
case overlay
case navigation
case compact
}
public final class CheckNode: ASDisplayNode {
@ -47,6 +48,9 @@ public final class CheckNode: ASDisplayNode {
case .navigation:
style = TGCheckButtonStyleGallery
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))!
checkView.setSelected(true, animated: false)

View File

@ -8,7 +8,8 @@ typedef enum
TGCheckButtonStyleMedia,
TGCheckButtonStyleGallery,
TGCheckButtonStyleShare,
TGCheckButtonStyleChat
TGCheckButtonStyleChat,
TGCheckButtonStyleCompact
} TGCheckButtonStyle;
@interface TGCheckButtonPallete : NSObject

View File

@ -106,6 +106,12 @@
}
break;
case TGCheckButtonStyleCompact:
{
insideInset = 6.0f;
}
break;
default:
{
insideInset = 5.0f;
@ -182,13 +188,18 @@
default:
{
CGFloat lineWidth = 1.0f;
if (style == TGCheckButtonStyleCompact) {
lineWidth = 1.5f;
}
CGRect rect = CGRectMake(0, 0, size.width, size.height);
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 1.0f);
CGContextSetLineWidth(context, lineWidth);
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();
UIGraphicsEndImageContext();
@ -234,7 +245,7 @@
UIColor *color = style == TGCheckButtonStyleDefaultBlue ? blueColor : greenColor;
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));
fillImage = UIGraphicsGetImageFromCurrentImageContext();

View File

@ -454,13 +454,15 @@ private final class SemanticStatusNodeDrawingState: NSObject {
let hollow: Bool
let transitionState: SemanticStatusNodeTransitionDrawingState?
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.foreground = foreground
self.hollow = hollow
self.transitionState = transitionState
self.drawingState = drawingState
self.cutout = cutout
super.init()
}
@ -481,6 +483,10 @@ private final class SemanticStatusNodeTransitionContext {
}
public final class SemanticStatusNode: ASControlNode {
final class Cutout {
}
public var backgroundNodeColor: UIColor {
didSet {
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))
}
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) {

View File

@ -6967,6 +6967,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let strongSelf = self {
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] = []
for item in results {
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: []))
}
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)
}
}
@ -6985,9 +7000,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if !messages.isEmpty {
if editingMessage {
strongSelf.editMessageMediaWithMessages(messages)
} else {
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }

View File

@ -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
}
} else if let image = media as? TelegramMediaImage {

View File

@ -1148,23 +1148,21 @@ 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)
var itemSelection: Bool?
if case .mosaic = prepareContentPosition {
switch content {
case .message:
break
case let .group(messages):
for (m, _, selection, _) in messages {
if m.id == message.id {
switch selection {
case .none:
break
case let .selectable(selected):
itemSelection = selected
}
break
switch content {
case .message:
break
case let .group(messages):
for (m, _, selection, _) in messages {
if m.id == message.id {
switch selection {
case .none:
break
case let .selectable(selected):
itemSelection = selected
}
break
}
}
}
}
let (properties, unboundSize, maxNodeWidth, nodeLayout) = prepareLayout(contentItem, layoutConstants, prepareContentPosition, itemSelection, CGSize(width: maximumContentWidth, height: CGFloat.greatestFiniteMagnitude))
@ -2777,7 +2775,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
}
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
} else {
if let parentSubnodes = parent.subnodes {

View File

@ -34,19 +34,21 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
self.addSubnode(self.interactiveFileNode)
self.interactiveFileNode.toggleSelection = { [weak self] value in
if let strongSelf = self, let item = strongSelf.item {
item.controllerInteraction.toggleMessagesSelection([item.message.id], value)
}
}
self.interactiveFileNode.activateLocalContent = { [weak self] in
if let strongSelf = self {
if let item = strongSelf.item {
let _ = item.controllerInteraction.openMessage(item.message, .default)
}
if let strongSelf = self, let item = strongSelf.item {
let _ = item.controllerInteraction.openMessage(item.message, .default)
}
}
self.interactiveFileNode.requestUpdateLayout = { [weak self] _ in
if let strongSelf = self {
if let item = strongSelf.item {
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id)
}
if let strongSelf = self, let item = strongSelf.item {
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id)
}
}
}
@ -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))) {
let interactiveFileLayout = self.interactiveFileNode.asyncLayout()
return { item, layoutConstants, preparePosition, _, constrainedSize in
return { item, layoutConstants, preparePosition, selection, constrainedSize in
var selectedFile: TelegramMediaFile?
for media in item.message.media {
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 (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)
@ -97,7 +99,15 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
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)
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 {
strongSelf.item = item

View File

@ -14,6 +14,7 @@ import TelegramStringFormatting
import RadialStatusNode
import SemanticStatusNode
import FileMediaResourceStatus
import CheckNode
private struct FetchControls {
let fetch: () -> Void
@ -21,6 +22,10 @@ private struct FetchControls {
}
final class ChatMessageInteractiveFileNode: ASDisplayNode {
private var selectionBackgroundNode: ASDisplayNode?
private var selectionNode: FileMessageSelectionNode?
private var cutoutNode: ASDisplayNode?
private let titleNode: TextNode
private let descriptionNode: TextNode
private let descriptionMeasuringNode: TextNode
@ -35,7 +40,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
private var iconNode: TransformImageNode?
private var statusNode: SemanticStatusNode?
private var playbackAudioLevelView: VoiceBlobView?
private var streamingStatusNode: RadialStatusNode?
private var streamingStatusNode: SemanticStatusNode?
private var tapRecognizer: UITapGestureRecognizer?
private let statusDisposable = MetaDisposable()
@ -76,6 +81,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
private var actualFetchStatus: MediaResourceStatus?
private let fetchDisposable = MetaDisposable()
var toggleSelection: (Bool) -> Void = { _ in }
var activateLocalContent: () -> Void = { }
var requestUpdateLayout: (Bool) -> Void = { _ in }
@ -86,8 +92,6 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
private var progressFrame: CGRect?
private var streamingCacheStatusFrame: CGRect?
private var fileIconImage: UIImage?
private var cloudFetchIconImage: UIImage?
private var cloudFetchedIconImage: UIImage?
override init() {
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 titleAsyncLayout = TextNode.asyncLayout(self.titleNode)
@ -214,7 +218,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
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
let titleFont = Font.regular(floor(presentationData.fontSize.baseDisplaySize * 16.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
}
let streamingProgressDiameter: CGFloat = 28.0
var hasStreamingProgress = false
if isAudio && !isVoice {
hasStreamingProgress = true
if hasStreamingProgress {
textConstrainedSize.width -= streamingProgressDiameter + 4.0
}
}
let streamingProgressDiameter: CGFloat = 20.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 (descriptionLayout, descriptionApply) = descriptionAsyncLayout(TextNodeLayoutArguments(attributedString: descriptionString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
@ -461,15 +457,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
if let statusSize = statusSize {
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?
if hasThumbnail {
fileIconImage = nil
@ -551,10 +539,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
}
if isAudio && !isVoice {
streamingCacheStatusFrame = CGRect(origin: CGPoint(x: boundingWidth - streamingProgressDiameter + 1.0, y: 8.0), size: CGSize(width: streamingProgressDiameter, height: streamingProgressDiameter))
if hasStreamingProgress {
fittedLayoutSize.width += streamingProgressDiameter + 6.0
}
streamingCacheStatusFrame = CGRect(origin: CGPoint(x: progressFrame.maxX - streamingProgressDiameter + 2.0, y: progressFrame.maxY - streamingProgressDiameter + 2.0), size: CGSize(width: streamingProgressDiameter, height: streamingProgressDiameter))
fittedLayoutSize.width = max(fittedLayoutSize.width, boundingWidth + 2.0)
} else {
streamingCacheStatusFrame = CGRect()
@ -688,7 +673,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
}
}))
}
strongSelf.waveformNode.displaysAsynchronously = !presentationData.isPreview
strongSelf.statusNode?.displaysAsynchronously = !presentationData.isPreview
strongSelf.statusNode?.frame = progressFrame
@ -696,8 +681,6 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
strongSelf.progressFrame = progressFrame
strongSelf.streamingCacheStatusFrame = streamingCacheStatusFrame
strongSelf.fileIconImage = fileIconImage
strongSelf.cloudFetchIconImage = cloudFetchIconImage
strongSelf.cloudFetchedIconImage = cloudFetchedIconImage
if let updatedFetchControls = 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
var streamingState: RadialStatusNodeState = .none
var streamingState: SemanticStatusNodeState = .none
let isSending = message.flags.isSending
@ -795,35 +835,19 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
}
if isAudio && !isVoice && !isSending {
let streamingStatusForegroundColor: UIColor = messageTheme.accentControlColor
let streamingStatusBackgroundColor: UIColor = messageTheme.mediaInactiveControlColor
switch resourceStatus.fetchStatus {
case let .Fetching(_, progress):
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:
if let cloudFetchedIconImage = self.cloudFetchedIconImage {
streamingState = .customIcon(cloudFetchedIconImage)
} else {
streamingState = .none
}
streamingState = .none
case .Remote:
if let cloudFetchIconImage = self.cloudFetchIconImage {
streamingState = .customIcon(cloudFetchIconImage)
} else {
streamingState = .none
}
streamingState = .download
}
} else {
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 {
case var .fetchStatus(fetchStatus):
if self.message?.forwardInfo != nil {
@ -903,7 +927,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
self.playbackAudioLevelView?.setColor(presentationData.theme.theme.chat.inputPanel.actionControlFillColor)
if streamingState != .none && self.streamingStatusNode == nil {
let streamingStatusNode = RadialStatusNode(backgroundNodeColor: .clear)
let streamingStatusNode = SemanticStatusNode(backgroundNodeColor: backgroundNodeColor, foregroundNodeColor: foregroundNodeColor)
self.streamingStatusNode = streamingStatusNode
streamingStatusNode.frame = streamingCacheStatusFrame
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 {
self.fetchingTextNode.attributedText = NSAttributedString(string: expandedString, 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)
}
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()
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 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 {
fileNode = node
@ -990,7 +1044,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
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
let (finalWidth, finalLayout) = continueLayout(constrainedSize)
@ -1053,3 +1107,60 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
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)
}
}