mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +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.";
|
||||
|
||||
"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)
|
||||
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))
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -8,7 +8,8 @@ typedef enum
|
||||
TGCheckButtonStyleMedia,
|
||||
TGCheckButtonStyleGallery,
|
||||
TGCheckButtonStyleShare,
|
||||
TGCheckButtonStyleChat
|
||||
TGCheckButtonStyleChat,
|
||||
TGCheckButtonStyleCompact
|
||||
} TGCheckButtonStyle;
|
||||
|
||||
@interface TGCheckButtonPallete : NSObject
|
||||
|
@ -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();
|
||||
|
@ -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) {
|
||||
|
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 {
|
||||
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) }
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user