From 7ac52c685dc8eb671c274905d8c7f845dad4317d Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sun, 29 Mar 2026 17:42:24 +0200 Subject: [PATCH 1/8] Various fixes --- .../Sources/Items/ItemListSwitchItem.swift | 9 +- .../Sources/PremiumBoostLevelsScreen.swift | 85 +++++++++++++------ 2 files changed, 65 insertions(+), 29 deletions(-) diff --git a/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift b/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift index 52db0c1cc7..bb6552e96f 100644 --- a/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift +++ b/submodules/ItemListUI/Sources/Items/ItemListSwitchItem.swift @@ -562,7 +562,14 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode { let iconOrigin: CGPoint switch item.systemStyle { case .glass: - iconOrigin = CGPoint(x: item.value ? switchFrame.maxX - icon.size.width - 16.0 + UIScreenPixel : switchFrame.minX + 16.0 - UIScreenPixel, y: switchFrame.minY + 8.0) + let offset: CGPoint + if #available(iOS 26.0, *) { + offset = .zero + } else { + offset = CGPoint(x: 5.0, y: 1.0) + } + + iconOrigin = CGPoint(x: item.value ? switchFrame.maxX - icon.size.width - 16.0 + UIScreenPixel + offset.x : switchFrame.minX + 16.0 - UIScreenPixel - offset.x, y: switchFrame.minY + 8.0 + offset.y) case .legacy: iconOrigin = CGPoint(x: item.value ? switchFrame.maxX - icon.size.width - 11.0 : switchFrame.minX + 11.0, y: switchFrame.minY + 9.0) } diff --git a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift index 04768d1aed..9190c1d178 100644 --- a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift @@ -36,6 +36,7 @@ private final class SheetContent: CombinedComponent { let mode: PremiumBoostLevelsScreen.Mode let status: ChannelBoostStatus? let boostState: InternalBoostState.DisplayData? + let bottomInset: CGFloat let boost: () -> Void let copyLink: (String) -> Void let dismiss: () -> Void @@ -52,6 +53,7 @@ private final class SheetContent: CombinedComponent { mode: PremiumBoostLevelsScreen.Mode, status: ChannelBoostStatus?, boostState: InternalBoostState.DisplayData?, + bottomInset: CGFloat, boost: @escaping () -> Void, copyLink: @escaping (String) -> Void, dismiss: @escaping () -> Void, @@ -67,6 +69,7 @@ private final class SheetContent: CombinedComponent { self.mode = mode self.status = status self.boostState = boostState + self.bottomInset = bottomInset self.boost = boost self.copyLink = copyLink self.dismiss = dismiss @@ -98,6 +101,9 @@ private final class SheetContent: CombinedComponent { if lhs.boostState != rhs.boostState { return false } + if lhs.bottomInset != rhs.bottomInset { + return false + } return true } @@ -134,7 +140,6 @@ private final class SheetContent: CombinedComponent { let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let theme = environment.theme let strings = environment.strings - let state = context.state let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 }) @@ -775,6 +780,7 @@ private final class SheetContent: CombinedComponent { contentSize.height += levels.size.height + 80.0 contentSize.height += 60.0 } + contentSize.height += component.bottomInset return contentSize } @@ -826,8 +832,8 @@ private final class PremiumBoostLevelsSheetComponent: CombinedComponent { final class State: ComponentState { private let context: AccountContext private let peerId: EnginePeer.Id - private let mode: PremiumBoostLevelsScreen.Mode - private let status: ChannelBoostStatus? + fileprivate let mode: PremiumBoostLevelsScreen.Mode + fileprivate let status: ChannelBoostStatus? private let myBoostStatus: MyBoostStatus? private let openPeer: ((EnginePeer) -> Void)? private let forceDark: Bool @@ -838,7 +844,7 @@ private final class PremiumBoostLevelsSheetComponent: CombinedComponent { private(set) var memberPeer: EnginePeer? private var peerDisposable: Disposable? - private var currentMyBoostCount: Int32 = 0 + fileprivate var currentMyBoostCount: Int32 = 0 private var myBoostCount: Int32 = 0 private var availableBoosts: [MyBoostStatus.Boost] = [] private var occupiedBoosts: [MyBoostStatus.Boost] = [] @@ -1235,7 +1241,9 @@ private final class PremiumBoostLevelsSheetComponent: CombinedComponent { let state = context.state let theme = environment.theme.withModalBlocksBackground() let strings = environment.strings - + + state.controller = controller() as? PremiumBoostLevelsScreen + let dismiss: (Bool) -> Void = { animated in if animated { animateOut.invoke(Action { _ in @@ -1338,6 +1346,24 @@ private final class PremiumBoostLevelsSheetComponent: CombinedComponent { ) } + var bottomButtonTitle: String? + if case .user = state.mode, state.status?.nextLevelBoosts != nil { + if state.currentMyBoostCount > 0 { + bottomButtonTitle = strings.ChannelBoost_BoostAgain + } else if state.isGroup == true { + bottomButtonTitle = strings.GroupBoost_BoostGroup + } else { + bottomButtonTitle = strings.ChannelBoost_BoostChannel + } + } + let bottomButtonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) + let contentBottomInset: CGFloat + if bottomButtonTitle != nil { + contentBottomInset = bottomButtonInsets.bottom + 52.0 + 16.0 + } else { + contentBottomInset = 0.0 + } + let sheet = sheet.update( component: ResizableSheetComponent( content: AnyComponent(SheetContent( @@ -1348,6 +1374,7 @@ private final class PremiumBoostLevelsSheetComponent: CombinedComponent { mode: context.component.mode, status: context.component.status, boostState: state.boostState, + bottomInset: contentBottomInset, boost: { [weak state] in state?.updateBoostState() }, @@ -1385,31 +1412,33 @@ private final class PremiumBoostLevelsSheetComponent: CombinedComponent { ) ), rightItem: rightItem, - bottomItem: "".isEmpty ? nil : AnyComponent( - ButtonComponent( - background: ButtonComponent.Background( - style: .glass, - color: theme.list.itemCheckColors.fillColor, - foreground: theme.list.itemCheckColors.foregroundColor, - pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) - ), - content: AnyComponentWithIdentity( - id: AnyHashable(0), - component: AnyComponent( - ButtonTextContentComponent( - text: strings.Common_OK, - badge: 0, - textColor: theme.list.itemCheckColors.foregroundColor, - badgeBackground: theme.list.itemCheckColors.foregroundColor, - badgeForeground: theme.list.itemCheckColors.fillColor + bottomItem: bottomButtonTitle.map { title in + AnyComponent( + ButtonComponent( + background: ButtonComponent.Background( + style: .glass, + color: theme.list.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ), + content: AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent( + ButtonTextContentComponent( + text: title, + badge: 0, + textColor: theme.list.itemCheckColors.foregroundColor, + badgeBackground: theme.list.itemCheckColors.foregroundColor, + badgeForeground: theme.list.itemCheckColors.fillColor + ) ) - ) - ), - action: { - dismiss(true) - } + ), + action: { [weak state] in + state?.updateBoostState() + } + ) ) - ), + }, backgroundColor: .color(theme.list.modalBlocksBackgroundColor), animateOut: animateOut ), From acc053a628a0eedc730012be6a0893a07c98766e Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sun, 29 Mar 2026 18:36:33 +0200 Subject: [PATCH 2/8] Various fixes --- .../Sources/TGMediaAssetsController.m | 76 ++++++++++++++++++- 1 file changed, 72 insertions(+), 4 deletions(-) diff --git a/submodules/LegacyComponents/Sources/TGMediaAssetsController.m b/submodules/LegacyComponents/Sources/TGMediaAssetsController.m index 4bd8b5a303..1522c48030 100644 --- a/submodules/LegacyComponents/Sources/TGMediaAssetsController.m +++ b/submodules/LegacyComponents/Sources/TGMediaAssetsController.m @@ -59,6 +59,74 @@ static TGMediaLivePhotoMode TGMediaAssetsResolvedLivePhotoMode(TGMediaEditingCon return editingContext.isForceLivePhotoEnabled ? TGMediaLivePhotoModeLive : TGMediaLivePhotoModeOff; } +static TGMediaLivePhotoMode TGMediaAssetsEffectiveLivePhotoSendMode(TGMediaEditingContext *editingContext, NSObject *item, id adjustments) +{ + TGMediaLivePhotoMode livePhotoMode = TGMediaAssetsResolvedLivePhotoMode(editingContext, item); + if (livePhotoMode == TGMediaLivePhotoModeLive && adjustments.paintingData.hasAnimation) + return TGMediaLivePhotoModeLoop; + + return livePhotoMode; +} + +static NSString *TGMediaAssetsLivePhotoPaintingImagePath(TGPaintingData *paintingData) +{ + if (paintingData.imagePath.length > 0) + return paintingData.imagePath; + + UIImage *paintingImage = paintingData.image; + if (paintingImage == nil) + return nil; + + NSData *paintingImageData = UIImagePNGRepresentation(paintingImage); + if (paintingImageData == nil) + return nil; + + int64_t randomId = 0; + arc4random_buf(&randomId, sizeof(randomId)); + NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSString alloc] initWithFormat:@"livephoto_painting_%llx.png", randomId]]; + [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; + if (![paintingImageData writeToFile:filePath atomically:true]) + return nil; + + return filePath; +} + +static TGVideoEditAdjustments *TGMediaAssetsPatchedLivePhotoAdjustments(PGPhotoEditorValues *values, TGMediaVideoConversionPreset preset, bool bounce, bool sendAsGif) +{ + TGVideoEditAdjustments *videoAdjustments = [TGVideoEditAdjustments editAdjustmentsWithPhotoEditorValues:values preset:preset bounce:bounce sendAsGif:sendAsGif]; + TGPaintingData *paintingData = values.paintingData; + NSString *paintingImagePath = TGMediaAssetsLivePhotoPaintingImagePath(paintingData); + if (paintingImagePath.length == 0) + return videoAdjustments; + + NSMutableDictionary *adjustmentsDictionary = [[videoAdjustments dictionary] mutableCopy]; + if (adjustmentsDictionary == nil) + return videoAdjustments; + + adjustmentsDictionary[@"paintingImagePath"] = paintingImagePath; + if (paintingData.stickers.count > 0) + adjustmentsDictionary[@"stickersData"] = paintingData.stickers; + + TGVideoEditAdjustments *dictionaryAdjustments = [TGVideoEditAdjustments editAdjustmentsWithDictionary:adjustmentsDictionary]; + TGPaintingData *patchedPaintingData = dictionaryAdjustments.paintingData; + if (patchedPaintingData == nil) { + if (paintingData.entitiesData != nil) { + patchedPaintingData = [TGPaintingData dataWithPaintingImagePath:paintingImagePath entitiesData:paintingData.entitiesData hasAnimation:paintingData.hasAnimation stickers:paintingData.stickers]; + } else { + patchedPaintingData = [TGPaintingData dataWithPaintingImagePath:paintingImagePath]; + } + } + + TGVideoEditAdjustments *patchedAdjustments = [TGVideoEditAdjustments editAdjustmentsWithOriginalSize:videoAdjustments.originalSize cropRect:videoAdjustments.cropRect cropOrientation:videoAdjustments.cropOrientation cropRotation:videoAdjustments.cropRotation cropLockedAspectRatio:videoAdjustments.cropLockedAspectRatio cropMirrored:videoAdjustments.cropMirrored trimStartValue:videoAdjustments.trimStartValue trimEndValue:videoAdjustments.trimEndValue toolValues:videoAdjustments.toolValues paintingData:patchedPaintingData sendAsGif:videoAdjustments.sendAsGif preset:videoAdjustments.preset]; + if (patchedAdjustments == nil) + return videoAdjustments; + + if (videoAdjustments.bounce != patchedAdjustments.bounce) + [patchedAdjustments setValue:@(videoAdjustments.bounce) forKey:@"bounce"]; + + return patchedAdjustments; +} + @interface TGMediaPickerAccessView: UIView { TGMediaAssetsPallete *_pallete; @@ -1022,7 +1090,6 @@ static TGMediaLivePhotoMode TGMediaAssetsResolvedLivePhotoMode(TGMediaEditingCon } NSAttributedString *caption = [editingContext captionForItem:asset]; - TGMediaLivePhotoMode livePhotoMode = TGMediaAssetsResolvedLivePhotoMode(editingContext, asset); if (editingContext.isForcedCaption) { if (grouping && num > 0) { @@ -1091,6 +1158,7 @@ static TGMediaLivePhotoMode TGMediaAssetsResolvedLivePhotoMode(TGMediaEditingCon else { id adjustments = [editingContext adjustmentsForItem:asset]; + TGMediaLivePhotoMode livePhotoMode = TGMediaAssetsEffectiveLivePhotoSendMode(editingContext, asset, adjustments); NSNumber *timer = [editingContext timerForItem:asset]; SSignal *inlineSignal = [inlineThumbnailSignal(asset) map:^id(UIImage *image) @@ -1222,20 +1290,20 @@ static TGMediaLivePhotoMode TGMediaAssetsResolvedLivePhotoMode(TGMediaEditingCon if (livePhotoMode == TGMediaLivePhotoModeBounce) { if ([adjustments isKindOfClass:[PGPhotoEditorValues class]]) { - dict[@"adjustments"] = [TGVideoEditAdjustments editAdjustmentsWithPhotoEditorValues:adjustments preset:TGMediaVideoConversionPresetCompressedHigh bounce:true sendAsGif:true]; + dict[@"adjustments"] = TGMediaAssetsPatchedLivePhotoAdjustments((PGPhotoEditorValues *)adjustments, TGMediaVideoConversionPresetCompressedHigh, true, true); } else { dict[@"adjustments"] = [TGVideoEditAdjustments editAdjustmentsWithOriginalSize:dimensions preset:TGMediaVideoConversionPresetCompressedHigh bounce:true]; } } else if (livePhotoMode == TGMediaLivePhotoModeLoop) { if ([adjustments isKindOfClass:[PGPhotoEditorValues class]]) { - dict[@"adjustments"] = [TGVideoEditAdjustments editAdjustmentsWithPhotoEditorValues:adjustments preset:TGMediaVideoConversionPresetCompressedHigh bounce:false sendAsGif:true]; + dict[@"adjustments"] = TGMediaAssetsPatchedLivePhotoAdjustments((PGPhotoEditorValues *)adjustments, TGMediaVideoConversionPresetCompressedHigh, false, true); } else { dict[@"adjustments"] = [TGVideoEditAdjustments editAdjustmentsWithOriginalSize:dimensions preset:TGMediaVideoConversionPresetCompressedHigh bounce:false]; } } else { dict[@"livePhoto"] = @true; if ([adjustments isKindOfClass:[PGPhotoEditorValues class]]) { - dict[@"adjustments"] = [TGVideoEditAdjustments editAdjustmentsWithPhotoEditorValues:adjustments preset:TGMediaVideoConversionPresetCompressedMedium bounce:false sendAsGif:false]; + dict[@"adjustments"] = TGMediaAssetsPatchedLivePhotoAdjustments((PGPhotoEditorValues *)adjustments, TGMediaVideoConversionPresetCompressedMedium, false, false); } } From 8d7b84caac607027ecf5d8160a247dabe5c9dce1 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sun, 29 Mar 2026 18:49:14 +0200 Subject: [PATCH 3/8] Various fixes --- .../TelegramCore/Sources/State/ApplyUpdateMessage.swift | 3 +++ .../Sources/ChatMessageMediaBubbleContentNode.swift | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift index a5249dc828..296bd1ebe2 100644 --- a/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift +++ b/submodules/TelegramCore/Sources/State/ApplyUpdateMessage.swift @@ -31,6 +31,9 @@ func applyMediaResourceChanges(from: Media, to: Media, postbox: Postbox, force: copyOrMoveResourceData(from: fromLargestRepresentation.resource, to: toLargestRepresentation.resource, mediaBox: postbox.mediaBox) } } + if let fromVideo = fromImage.video, let toVideo = toImage.video { + applyMediaResourceChanges(from: fromVideo, to: toVideo, postbox: postbox, force: true) + } } else if let fromFile = from as? TelegramMediaFile, let toFile = to as? TelegramMediaFile { if !skipPreviews { if let fromPreview = smallestImageRepresentation(fromFile.previewRepresentations), let toPreview = smallestImageRepresentation(toFile.previewRepresentations) { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift index 63ade6626a..a979e9f00f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift @@ -122,6 +122,7 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { var automaticDownload: InteractiveMediaNodeAutodownloadMode = .none var automaticPlayback: Bool = false var contentMode: InteractiveMediaNodeContentMode = .aspectFit + var isLivePhoto = false if let updatingMedia = item.attributes.updatingMedia, case let .update(mediaReference) = updatingMedia.media { selectedMedia = mediaReference.media @@ -136,6 +137,7 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { if let _ = telegramImage.video { automaticPlayback = true + isLivePhoto = true } } else if let telegramStory = media as? TelegramMediaStory { selectedMedia = telegramStory @@ -432,7 +434,7 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { var wideLayout = true if case let .mosaic(_, wide) = position { wideLayout = wide - automaticPlayback = automaticPlayback && wide + automaticPlayback = automaticPlayback && (wide || isLivePhoto) } var updatedPosition: ChatMessageBubbleContentPosition = position From f3c18e1436f1942d68b684c35b1263109884cd7a Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sun, 29 Mar 2026 22:18:19 +0200 Subject: [PATCH 4/8] Various improvements --- .../Sources/ListMessageFileItemNode.swift | 14 ++-- .../Sources/ListMessageSnippetItemNode.swift | 2 +- .../State/AccountStateManagementUtils.swift | 2 + .../Sources/Statistics/StoryStatistics.swift | 1 + .../Messages/EngineStoryViewListContext.swift | 4 + .../Messages/PendingStoryManager.swift | 21 ++++- .../TelegramEngine/Messages/Stories.swift | 77 ++++++++++++++++++- .../Messages/StoryListContext.swift | 52 ++++++++++++- .../Messages/TelegramEngineMessages.swift | 5 +- .../Sources/AttachmentFileSearchItem.swift | 70 +++++++++++------ .../Sources/MediaEditorScreen.swift | 5 ++ .../Sources/MediaEditorStoryCompletion.swift | 11 +-- .../Sources/StoryChatContent.swift | 4 + .../StoryContentCaptionComponent.swift | 6 ++ .../StoryItemSetContainerComponent.swift | 1 + .../OverlayAudioPlayerControllerNode.swift | 52 ++++++++----- .../Sources/TelegramRootController.swift | 1 + 17 files changed, 261 insertions(+), 67 deletions(-) diff --git a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift index 63b601b3e9..4505aaf2dc 100644 --- a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift @@ -211,17 +211,17 @@ public final class ListMessageFileItemNode: ListMessageNode { self.addSubnode(self.descriptionNode) } - func asyncLayout() -> (_ context: AccountContext, _ constrainedWidth: CGFloat, _ theme: PresentationTheme, _ authorTitle: NSAttributedString?, _ topic: (title: NSAttributedString, showIcon: Bool, iconId: Int64?, iconColor: Int32)?) -> (CGSize, () -> Void) { + func asyncLayout() -> (_ context: AccountContext, _ constrainedWidth: CGFloat, _ theme: PresentationTheme, _ authorTitle: NSAttributedString?, _ truncateType: CTLineTruncationType, _ topic: (title: NSAttributedString, showIcon: Bool, iconId: Int64?, iconColor: Int32)?) -> (CGSize, () -> Void) { let makeDescriptionLayout = TextNode.asyncLayout(self.descriptionNode) let makeTopicTitleLayout = TextNode.asyncLayout(self.topicTitleNode) - return { [weak self] context, constrainedWidth, theme, authorTitle, topic in + return { [weak self] context, constrainedWidth, theme, authorTitle, truncateType, topic in var maxTitleWidth = constrainedWidth if let _ = topic { maxTitleWidth = floor(constrainedWidth * 0.7) } - let descriptionLayout = makeDescriptionLayout(TextNodeLayoutArguments(attributedString: authorTitle, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0))) + let descriptionLayout = makeDescriptionLayout(TextNodeLayoutArguments(attributedString: authorTitle, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: truncateType, constrainedSize: CGSize(width: maxTitleWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 1.0, bottom: 2.0, right: 1.0))) var remainingWidth = constrainedWidth - descriptionLayout.0.size.width @@ -1040,15 +1040,15 @@ public final class ListMessageFileItemNode: ListMessageNode { let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) let dateText = stringForRelativeTimestamp(strings: item.presentationData.strings, relativeTimestamp: item.message?.timestamp ?? 0, relativeTo: timestamp, dateTimeFormat: item.presentationData.dateTimeFormat) - let dateAttributedString = NSAttributedString(string: dateText, font: dateFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor) + let dateAttributedString = !item.isGlobalSearchResult ? NSAttributedString() : NSAttributedString(string: dateText, font: dateFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor) let (dateNodeLayout, dateNodeApply) = dateNodeMakeLayout(TextNodeLayoutArguments(attributedString: dateAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - leftOffset - contentRightInset - 12.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(item.context, params.width - leftInset - leftOffset - contentRightInset - dateNodeLayout.size.width - 4.0, item.presentationData.theme.theme, titleText, titleExtraData) + let (titleNodeLayout, titleNodeApply) = titleNodeMakeLayout(item.context, params.width - leftInset - leftOffset - contentRightInset - dateNodeLayout.size.width - 4.0, item.presentationData.theme.theme, titleText, isAudio ? .end : .middle, titleExtraData) let (textNodeLayout, textNodeApply) = textNodeMakeLayout(TextNodeLayoutArguments(attributedString: captionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - leftOffset - contentRightInset - 30.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(item.context, params.width - leftInset - leftOffset - contentRightInset - 30.0, item.presentationData.theme.theme, descriptionText, descriptionExtraData) + let (descriptionNodeLayout, descriptionNodeApply) = descriptionNodeMakeLayout(item.context, params.width - leftInset - leftOffset - contentRightInset - 30.0, item.presentationData.theme.theme, descriptionText, .middle, descriptionExtraData) var (extensionTextLayout, extensionTextApply) = extensionIconTextMakeLayout(TextNodeLayoutArguments(attributedString: extensionText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 38.0, height: CGFloat.infinity), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) if extensionTextLayout.truncated, let text = extensionText?.string { @@ -1168,7 +1168,7 @@ public final class ListMessageFileItemNode: ListMessageNode { strongSelf.backgroundNode = backgroundNode strongSelf.insertSubnode(backgroundNode, at: 0) } - backgroundNode.backgroundColor = item.canReorder ? item.presentationData.theme.theme.list.plainBackgroundColor : item.presentationData.theme.theme.list.itemBlocksBackgroundColor + backgroundNode.backgroundColor = item.canReorder ? item.presentationData.theme.theme.list.itemModalBlocksBackgroundColor : item.presentationData.theme.theme.list.itemBlocksBackgroundColor } strongSelf.separatorNode.backgroundColor = item.presentationData.theme.theme.list.itemPlainSeparatorColor diff --git a/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift index 9a72ae2d22..b743aaf3fe 100644 --- a/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift @@ -672,7 +672,7 @@ public final class ListMessageSnippetItemNode: ListMessageNode { } let authorText = NSAttributedString(string: authorString, font: authorFont, textColor: item.presentationData.theme.theme.list.itemSecondaryTextColor) - let (authorNodeLayout, authorNodeApply) = authorNodeMakeLayout(item.context, params.width - leftInset - leftOffset - contentRightInset - 30.0, item.presentationData.theme.theme, authorText, forumThreadTitle) + let (authorNodeLayout, authorNodeApply) = authorNodeMakeLayout(item.context, params.width - leftInset - leftOffset - contentRightInset - 30.0, item.presentationData.theme.theme, authorText, .middle, forumThreadTitle) var contentHeight = 9.0 + titleNodeLayout.size.height + 10.0 + descriptionNodeLayout.size.height + linkNodeLayout.size.height if !authorString.isEmpty { diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 393b23d09f..555b9d62cb 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -5457,6 +5457,7 @@ func replayFinalState( isMy: item.isMy, myReaction: updatedReaction, forwardInfo: item.forwardInfo, + music: item.music, authorId: item.authorId, folderIds: item.folderIds )) @@ -5492,6 +5493,7 @@ func replayFinalState( isMy: item.isMy, myReaction: MessageReaction.Reaction(apiReaction: reaction), forwardInfo: item.forwardInfo, + music: item.music, authorId: item.authorId, folderIds: item.folderIds )) diff --git a/submodules/TelegramCore/Sources/Statistics/StoryStatistics.swift b/submodules/TelegramCore/Sources/Statistics/StoryStatistics.swift index d4610f4f05..14a5a7945f 100644 --- a/submodules/TelegramCore/Sources/Statistics/StoryStatistics.swift +++ b/submodules/TelegramCore/Sources/Statistics/StoryStatistics.swift @@ -354,6 +354,7 @@ private final class StoryStatsPublicForwardsContextImpl { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) }, + music: item.music.flatMap(EngineMedia.init), author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) }, folderIds: item.folderIds ) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift index b54e699ff8..d7d6352b2e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift @@ -563,6 +563,7 @@ public final class EngineStoryViewListContext { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) }, + music: item.music.flatMap(EngineMedia.init), author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) }, folderIds: item.folderIds ), @@ -604,6 +605,7 @@ public final class EngineStoryViewListContext { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo, + music: item.music, authorId: item.authorId, folderIds: item.folderIds )) @@ -645,6 +647,7 @@ public final class EngineStoryViewListContext { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo, + music: item.music, authorId: item.authorId, folderIds: item.folderIds )) @@ -767,6 +770,7 @@ public final class EngineStoryViewListContext { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) }, + music: item.music.flatMap(EngineMedia.init), author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) }, folderIds: item.folderIds ), diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift index 5d28604455..0859aeb5c7 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift @@ -112,8 +112,9 @@ public extension Stories { case period case randomId case forwardInfo - case uploadInfo case folders + case music + case uploadInfo } public let target: PendingTarget @@ -131,6 +132,7 @@ public extension Stories { public let randomId: Int64 public let forwardInfo: PendingForwardInfo? public let folders: [Int64] + public let music: TelegramMediaFile? public let uploadInfo: StoryUploadInfo? public init( @@ -149,6 +151,7 @@ public extension Stories { randomId: Int64, forwardInfo: PendingForwardInfo?, folders: [Int64], + music: TelegramMediaFile?, uploadInfo: StoryUploadInfo? ) { self.target = target @@ -166,6 +169,7 @@ public extension Stories { self.randomId = randomId self.forwardInfo = forwardInfo self.folders = folders + self.music = music self.uploadInfo = uploadInfo } @@ -197,6 +201,9 @@ public extension Stories { self.folders = try container.decodeIfPresent([Int64].self, forKey: .folders) ?? [] + let musicData = try container.decode(Data.self, forKey: .music) + self.music = PostboxDecoder(buffer: MemoryBuffer(data: musicData)).decodeRootObject() as? TelegramMediaFile + self.uploadInfo = try container.decodeIfPresent(StoryUploadInfo.self, forKey: .uploadInfo) } @@ -226,6 +233,13 @@ public extension Stories { try container.encode(self.period, forKey: .period) try container.encode(self.randomId, forKey: .randomId) try container.encodeIfPresent(self.forwardInfo, forKey: .forwardInfo) + + if let music = self.music { + let musicEncoder = PostboxEncoder() + musicEncoder.encodeRootObject(music) + try container.encode(musicEncoder.makeData(), forKey: .music) + } + try container.encode(self.folders, forKey: .folders) try container.encodeIfPresent(self.uploadInfo, forKey: .uploadInfo) } @@ -270,6 +284,9 @@ public extension Stories { if lhs.folders != rhs.folders { return false } + if lhs.music != rhs.music { + return false + } if lhs.uploadInfo != rhs.uploadInfo { return false } @@ -519,7 +536,7 @@ final class PendingStoryManager { let partTotalProgress = 1.0 / Float(uploadInfo.total) pendingItemContext.progress = Float(uploadInfo.index) * partTotalProgress } - pendingItemContext.disposable = (_internal_uploadStoryImpl(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, stateManager: self.stateManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, revalidationContext: self.revalidationContext, auxiliaryMethods: self.auxiliaryMethods, toPeerId: toPeerId, stableId: stableId, media: firstItem.media, mediaAreas: firstItem.mediaAreas, text: firstItem.text, entities: firstItem.entities, embeddedStickers: firstItem.embeddedStickers, pin: firstItem.pin, privacy: firstItem.privacy, isForwardingDisabled: firstItem.isForwardingDisabled, period: Int(firstItem.period), folders: firstItem.folders, randomId: firstItem.randomId, forwardInfo: firstItem.forwardInfo) + pendingItemContext.disposable = (_internal_uploadStoryImpl(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, stateManager: self.stateManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, revalidationContext: self.revalidationContext, auxiliaryMethods: self.auxiliaryMethods, toPeerId: toPeerId, stableId: stableId, media: firstItem.media, mediaAreas: firstItem.mediaAreas, text: firstItem.text, entities: firstItem.entities, embeddedStickers: firstItem.embeddedStickers, pin: firstItem.pin, privacy: firstItem.privacy, isForwardingDisabled: firstItem.isForwardingDisabled, period: Int(firstItem.period), folders: firstItem.folders, music: firstItem.music, randomId: firstItem.randomId, forwardInfo: firstItem.forwardInfo) |> deliverOn(self.queue)).start(next: { [weak self] event in guard let `self` = self else { return diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 065cf8fe7b..fc104f5fb9 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -262,6 +262,7 @@ public enum Stories { case isMy case myReaction case forwardInfo + case music case authorId case folderIds } @@ -287,6 +288,7 @@ public enum Stories { public let isMy: Bool public let myReaction: MessageReaction.Reaction? public let forwardInfo: ForwardInfo? + public let music: TelegramMediaFile? public let authorId: PeerId? public let folderIds: [Int64]? @@ -316,6 +318,7 @@ public enum Stories { isMy: Bool, myReaction: MessageReaction.Reaction?, forwardInfo: ForwardInfo?, + music: TelegramMediaFile?, authorId: PeerId?, folderIds: [Int64]? ) { @@ -340,6 +343,7 @@ public enum Stories { self.isMy = isMy self.myReaction = myReaction self.forwardInfo = forwardInfo + self.music = music self.authorId = authorId self.folderIds = folderIds } @@ -388,6 +392,13 @@ public enum Stories { self.isMy = try container.decodeIfPresent(Bool.self, forKey: .isMy) ?? false self.myReaction = try container.decodeIfPresent(MessageReaction.Reaction.self, forKey: .myReaction) self.forwardInfo = try container.decodeIfPresent(ForwardInfo.self, forKey: .forwardInfo) + + if let musicData = try container.decodeIfPresent(Data.self, forKey: .music) { + self.music = PostboxDecoder(buffer: MemoryBuffer(data: musicData)).decodeRootObject() as? TelegramMediaFile + } else { + self.music = nil + } + self.authorId = try container.decodeIfPresent(Int64.self, forKey: .authorId).flatMap { PeerId($0) } self.folderIds = try container.decodeIfPresent([Int64].self, forKey: .folderIds) } @@ -430,6 +441,14 @@ public enum Stories { try container.encode(self.isMy, forKey: .isMy) try container.encodeIfPresent(self.myReaction, forKey: .myReaction) try container.encodeIfPresent(self.forwardInfo, forKey: .forwardInfo) + + if let music = self.music { + let encoder = PostboxEncoder() + encoder.encodeRootObject(music) + let musicData = encoder.makeData() + try container.encode(musicData, forKey: .music) + } + try container.encodeIfPresent(self.authorId?.toInt64(), forKey: .authorId) try container.encodeIfPresent(self.folderIds, forKey: .folderIds) } @@ -504,6 +523,15 @@ public enum Stories { if lhs.forwardInfo != rhs.forwardInfo { return false } + if let lhsMusic = lhs.music, let rhsMusic = rhs.music { + if !lhsMusic.isEqual(to: rhsMusic) { + return false + } + } else { + if (lhs.music == nil) != (rhs.music == nil) { + return false + } + } if lhs.authorId != rhs.authorId { return false } @@ -1035,7 +1063,23 @@ public struct StoryUploadInfo: Codable, Equatable { } } -func _internal_uploadStory(account: Account, target: Stories.PendingTarget, media: EngineStoryInputMedia, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, period: Int, randomId: Int64, forwardInfo: Stories.PendingForwardInfo?, folders: [Int64], uploadInfo: StoryUploadInfo? = nil) -> Signal { +func _internal_uploadStory( + account: Account, + target: Stories.PendingTarget, + media: EngineStoryInputMedia, + mediaAreas: [MediaArea], + text: String, + entities: [MessageTextEntity], + pin: Bool, + privacy: EngineStoryPrivacy, + isForwardingDisabled: Bool, + period: Int, + randomId: Int64, + forwardInfo: Stories.PendingForwardInfo?, + folders: [Int64], + music: TelegramMediaFile?, + uploadInfo: StoryUploadInfo? = nil +) -> Signal { let inputMedia = prepareUploadStoryContent(account: account, media: media) return (account.postbox.transaction { transaction in @@ -1065,6 +1109,7 @@ func _internal_uploadStory(account: Account, target: Stories.PendingTarget, medi randomId: randomId, forwardInfo: forwardInfo, folders: folders, + music: music, uploadInfo: uploadInfo )) transaction.setLocalStoryState(state: CodableEntry(currentState)) @@ -1113,6 +1158,7 @@ func _internal_cancelStoryUpload(account: Account, stableId: Int32) { randomId: currentState.items[i].randomId, forwardInfo: currentState.items[i].forwardInfo, folders: currentState.items[i].folders, + music: currentState.items[i].music, uploadInfo: StoryUploadInfo( groupingId: groupingId, index: newIndex, @@ -1203,6 +1249,7 @@ func _internal_beginStoryLivestream(account: Account, peerId: EnginePeer.Id, rtm isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) }, + music: item.music.flatMap(EngineMedia.init), author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) }, folderIds: item.folderIds ) @@ -1279,6 +1326,7 @@ func _internal_uploadStoryImpl( isForwardingDisabled: Bool, period: Int, folders: [Int64], + music: TelegramMediaFile?, randomId: Int64, forwardInfo: Stories.PendingForwardInfo? ) -> Signal { @@ -1373,7 +1421,11 @@ func _internal_uploadStoryImpl( flags |= 1 << 8 } - //var apiMusic: Api.InputDocument? + var apiMusic: Api.InputDocument? + if let resource = music?.resource as? CloudDocumentMediaResource, let fileReference = resource.fileReference { + flags |= 1 << 9 + apiMusic = .inputDocument(.init(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: fileReference))) + } return network.request(Api.functions.stories.sendStory( flags: flags, @@ -1388,7 +1440,7 @@ func _internal_uploadStoryImpl( fwdFromId: fwdFromId, fwdFromStory: fwdFromStory, albums: folders.isEmpty ? nil : folders.map(Int32.init(clamping:)), - music: nil + music: apiMusic )) |> map(Optional.init) |> `catch` { _ -> Signal in @@ -1441,6 +1493,7 @@ func _internal_uploadStoryImpl( isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo, + music: item.music, authorId: fromId?.peerId, folderIds: item.folderIds ) @@ -1861,6 +1914,7 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo, + music: item.music, authorId: item.authorId, folderIds: item.folderIds ) @@ -1894,6 +1948,7 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo, + music: item.music, authorId: item.authorId, folderIds: item.folderIds ) @@ -2092,6 +2147,7 @@ func _internal_updateStoriesArePinned(account: Account, peerId: PeerId, ids: [In isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo, + music: item.music, authorId: item.authorId, folderIds: item.folderIds ) @@ -2124,6 +2180,7 @@ func _internal_updateStoriesArePinned(account: Account, peerId: PeerId, ids: [In isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo, + music: item.music, authorId: item.authorId, folderIds: item.folderIds ) @@ -2251,7 +2308,7 @@ extension Stories.StoredItem { init?(apiStoryItem: Api.StoryItem, existingItem: Stories.Item? = nil, peerId: PeerId, transaction: Transaction) { switch apiStoryItem { case let .storyItem(storyItemData): - let (flags, id, date, fromId, forwardFrom, expireDate, caption, entities, media, mediaAreas, privacy, views, sentReaction, albums) = (storyItemData.flags, storyItemData.id, storyItemData.date, storyItemData.fromId, storyItemData.fwdFrom, storyItemData.expireDate, storyItemData.caption, storyItemData.entities, storyItemData.media, storyItemData.mediaAreas, storyItemData.privacy, storyItemData.views, storyItemData.sentReaction, storyItemData.albums) + let (flags, id, date, fromId, forwardFrom, expireDate, caption, entities, media, mediaAreas, privacy, views, sentReaction, albums, music) = (storyItemData.flags, storyItemData.id, storyItemData.date, storyItemData.fromId, storyItemData.fwdFrom, storyItemData.expireDate, storyItemData.caption, storyItemData.entities, storyItemData.media, storyItemData.mediaAreas, storyItemData.privacy, storyItemData.views, storyItemData.sentReaction, storyItemData.albums, storyItemData.music) var folderIds: [Int64]? if let albums { folderIds = albums.map(Int64.init) @@ -2350,6 +2407,12 @@ extension Stories.StoredItem { break } + + var parsedMusic: TelegramMediaFile? + if let music { + parsedMusic = telegramMediaFileFromApiDocument(music, altDocuments: nil) + } + let item = Stories.Item( id: id, timestamp: date, @@ -2372,6 +2435,7 @@ extension Stories.StoredItem { isMy: mergedIsMy, myReaction: mergedMyReaction, forwardInfo: mergedForwardInfo, + music: parsedMusic, authorId: fromId?.peerId, folderIds: folderIds ) @@ -2453,6 +2517,7 @@ func _internal_getStoryById(accountPeerId: PeerId, postbox: Postbox, network: Ne isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) }, + music: item.music.flatMap(EngineMedia.init), author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) }, folderIds: item.folderIds ) @@ -2953,6 +3018,7 @@ func _internal_setStoryReaction(account: Account, peerId: EnginePeer.Id, id: Int isMy: item.isMy, myReaction: reaction, forwardInfo: item.forwardInfo, + music: item.music, authorId: item.authorId, folderIds: item.folderIds )) @@ -2988,6 +3054,7 @@ func _internal_setStoryReaction(account: Account, peerId: EnginePeer.Id, id: Int isMy: item.isMy, myReaction: reaction, forwardInfo: item.forwardInfo, + music: item.music, authorId: item.authorId, folderIds: item.folderIds )) @@ -3065,6 +3132,7 @@ func _internal_sendStoryStars(account: Account, peerId: EnginePeer.Id, id: Int32 isMy: item.isMy, myReaction: .stars, forwardInfo: item.forwardInfo, + music: item.music, authorId: item.authorId, folderIds: item.folderIds )) @@ -3100,6 +3168,7 @@ func _internal_sendStoryStars(account: Account, peerId: EnginePeer.Id, id: Int32 isMy: item.isMy, myReaction: .stars, forwardInfo: item.forwardInfo, + music: item.music, authorId: item.authorId, folderIds: item.folderIds )) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index af801abd0c..9e55f11f9a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -85,10 +85,37 @@ public final class EngineStoryItem: Equatable { public let isMy: Bool public let myReaction: MessageReaction.Reaction? public let forwardInfo: ForwardInfo? + public let music: EngineMedia? public let author: EnginePeer? public let folderIds: [Int64]? - public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, alternativeMediaList: [EngineMedia], mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool, isPending: Bool, isCloseFriends: Bool, isContacts: Bool, isSelectedContacts: Bool, isForwardingDisabled: Bool, isEdited: Bool, isMy: Bool, myReaction: MessageReaction.Reaction?, forwardInfo: ForwardInfo?, author: EnginePeer?, folderIds: [Int64]?) { + public init( + id: Int32, + timestamp: Int32, + expirationTimestamp: Int32, + media: EngineMedia, + alternativeMediaList: [EngineMedia], + mediaAreas: [MediaArea], + text: String, + entities: [MessageTextEntity], + views: Views?, + privacy: EngineStoryPrivacy?, + isPinned: Bool, + isExpired: Bool, + isPublic: Bool, + isPending: Bool, + isCloseFriends: Bool, + isContacts: Bool, + isSelectedContacts: Bool, + isForwardingDisabled: Bool, + isEdited: Bool, + isMy: Bool, + myReaction: MessageReaction.Reaction?, + forwardInfo: ForwardInfo?, + music: EngineMedia?, + author: EnginePeer?, + folderIds: [Int64]? + ) { self.id = id self.timestamp = timestamp self.expirationTimestamp = expirationTimestamp @@ -111,6 +138,7 @@ public final class EngineStoryItem: Equatable { self.isMy = isMy self.myReaction = myReaction self.forwardInfo = forwardInfo + self.music = music self.author = author self.folderIds = folderIds } @@ -182,6 +210,9 @@ public final class EngineStoryItem: Equatable { if lhs.forwardInfo != rhs.forwardInfo { return false } + if lhs.music != rhs.music { + return false + } if lhs.author != rhs.author { return false } @@ -239,9 +270,9 @@ public extension EngineStoryItem { isForwardingDisabled: self.isForwardingDisabled, isEdited: self.isEdited, isMy: self.isMy, - myReaction: self.myReaction, forwardInfo: self.forwardInfo?.storedForwardInfo, + music: self.music?._asMedia() as? TelegramMediaFile, authorId: self.author?.id, folderIds: self.folderIds ) @@ -744,6 +775,7 @@ public final class PeerStoryListContext: StoryListContext { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) }, + music: item.music.flatMap(EngineMedia.init), author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) }, folderIds: item.folderIds ) @@ -966,6 +998,7 @@ public final class PeerStoryListContext: StoryListContext { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) }, + music: item.music.flatMap(EngineMedia.init), author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) }, folderIds: item.folderIds ) @@ -1161,6 +1194,7 @@ public final class PeerStoryListContext: StoryListContext { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, peers: peers) }, + music: item.music.flatMap(EngineMedia.init), author: item.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) }, folderIds: item.folderIds ), peer: nil) @@ -1211,6 +1245,7 @@ public final class PeerStoryListContext: StoryListContext { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, peers: peers) }, + music: item.music.flatMap(EngineMedia.init), author: item.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) }, folderIds: item.folderIds ), peer: nil) @@ -1263,6 +1298,7 @@ public final class PeerStoryListContext: StoryListContext { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, peers: peers) }, + music: item.music.flatMap(EngineMedia.init), author: item.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) }, folderIds: item.folderIds ), peer: nil)) @@ -1321,6 +1357,7 @@ public final class PeerStoryListContext: StoryListContext { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, peers: peers) }, + music: item.music.flatMap(EngineMedia.init), author: item.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) }, folderIds: item.folderIds ), peer: nil)) @@ -1656,6 +1693,7 @@ public final class PeerStoryListContext: StoryListContext { return .unknown(name: name, isModified: isModified) } }, + music: item.music?._asMedia() as? TelegramMediaFile, authorId: item.author?.id, folderIds: item.folderIds ) @@ -2001,6 +2039,7 @@ public final class PeerStoryListContext: StoryListContext { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) }, + music: item.music.flatMap(EngineMedia.init), author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) }, folderIds: item.folderIds ) @@ -2201,6 +2240,7 @@ public final class SearchStoryListContext: StoryListContext { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) }, + music: item.music.flatMap(EngineMedia.init), author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) }, folderIds: item.folderIds ) @@ -2351,6 +2391,7 @@ public final class SearchStoryListContext: StoryListContext { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, peers: peers) }, + music: item.music.flatMap(EngineMedia.init), author: item.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) }, folderIds: item.folderIds ), @@ -2413,6 +2454,7 @@ public final class SearchStoryListContext: StoryListContext { isMy: item.storyItem.isMy, myReaction: reaction, forwardInfo: item.storyItem.forwardInfo, + music: item.storyItem.music, author: item.storyItem.author, folderIds: item.storyItem.folderIds ), @@ -2543,6 +2585,7 @@ public final class PeerExpiringStoryListContext { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) }, + music: item.music.flatMap(EngineMedia.init), author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) }, folderIds: item.folderIds ) @@ -3008,6 +3051,7 @@ public final class BotPreviewStoryListContext: StoryListContext { isMy: false, myReaction: nil, forwardInfo: nil, + music: nil, author: nil, folderIds: nil ), @@ -3058,6 +3102,7 @@ public final class BotPreviewStoryListContext: StoryListContext { isMy: false, myReaction: nil, forwardInfo: nil, + music: nil, author: nil, folderIds: nil ), @@ -3171,6 +3216,7 @@ public final class BotPreviewStoryListContext: StoryListContext { isMy: false, myReaction: nil, forwardInfo: nil, + music: nil, author: nil, folderIds: nil ), @@ -3249,6 +3295,7 @@ public final class BotPreviewStoryListContext: StoryListContext { isMy: false, myReaction: nil, forwardInfo: nil, + music: nil, author: nil, folderIds: nil ), @@ -3313,6 +3360,7 @@ public final class BotPreviewStoryListContext: StoryListContext { isMy: false, myReaction: nil, forwardInfo: nil, + music: nil, author: nil, folderIds: nil ), diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 8543ebfa2c..5740b8b987 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -1469,6 +1469,7 @@ public extension TelegramEngine { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo, + music: item.music, authorId: item.authorId, folderIds: item.folderIds )) @@ -1484,8 +1485,8 @@ public extension TelegramEngine { } } - public func uploadStory(target: Stories.PendingTarget, media: EngineStoryInputMedia, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, period: Int, randomId: Int64, forwardInfo: Stories.PendingForwardInfo?, folders: [Int64], uploadInfo: StoryUploadInfo? = nil) -> Signal { - return _internal_uploadStory(account: self.account, target: target, media: media, mediaAreas: mediaAreas, text: text, entities: entities, pin: pin, privacy: privacy, isForwardingDisabled: isForwardingDisabled, period: period, randomId: randomId, forwardInfo: forwardInfo, folders: folders, uploadInfo: uploadInfo) + public func uploadStory(target: Stories.PendingTarget, media: EngineStoryInputMedia, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], pin: Bool, privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, period: Int, randomId: Int64, forwardInfo: Stories.PendingForwardInfo?, folders: [Int64], music: TelegramMediaFile?, uploadInfo: StoryUploadInfo? = nil) -> Signal { + return _internal_uploadStory(account: self.account, target: target, media: media, mediaAreas: mediaAreas, text: text, entities: entities, pin: pin, privacy: privacy, isForwardingDisabled: isForwardingDisabled, period: period, randomId: randomId, forwardInfo: forwardInfo, folders: folders, music: music, uploadInfo: uploadInfo) } public func beginStoryLivestream(peerId: EnginePeer.Id, rtmp: Bool, privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, messagesEnabled: Bool, sendPaidMessageStars: Int64?) -> Signal { diff --git a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift index d9931d830c..f3edb35f99 100644 --- a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift +++ b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift @@ -500,12 +500,7 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon let searchQuery = self.searchQuery.get() |> mapToSignal { query -> Signal in - if let query = query, !query.isEmpty { - return (.complete() |> delay(0.6, queue: Queue.mainQueue())) - |> then(.single(query)) - } else { - return .single(query) - } + return .single(query) } let expandedSectionsPromise = self.expandedSectionsPromise @@ -525,6 +520,7 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon shared = .single(nil) |> then( context.engine.messages.searchMessages(location: .sentMedia(tags: [.file]), query: query, state: nil) + |> delay(0.6, queue: Queue.mainQueue()) |> map { result -> [Message]? in return result.0.messages } @@ -535,13 +531,15 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon shared = .single(nil) |> then( context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: [.music], minDate: nil, maxDate: nil), query: query, state: nil) + |> delay(0.6, queue: Queue.mainQueue()) |> map { result -> [Message]? in - return result.0.messages + return result.0.messages.filter { !$0.isRestricted(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) } } ) savedMusic = .single(nil) |> then( savedMusicContext!.state + |> delay(0.6, queue: Queue.mainQueue()) |> map { state in let peerId = context.account.peerId var messages: [Message] = [] @@ -571,13 +569,12 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon return messages } ) - - let trimmedQuery = query.trimmingCharacters(in: .whitespacesAndNewlines) - - if let data = context.currentAppConfiguration.with({ $0 }).data, let searchBot = data["music_search_username"] as? String, !searchBot.isEmpty, trimmedQuery.count >= 3 { + + if let data = context.currentAppConfiguration.with({ $0 }).data, let searchBot = data["music_search_username"] as? String, !searchBot.isEmpty, query.count >= 3 { globalMusic = .single(nil) |> then( context.engine.peers.resolvePeerByName(name: searchBot, referrer: nil) + |> delay(0.6, queue: Queue.mainQueue()) |> mapToSignal { result -> Signal in guard case let .result(result) = result else { return .complete() @@ -588,7 +585,7 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon guard let peer = peer else { return .single(nil) } - return context.engine.messages.requestChatContextResults(botId: peer.id, peerId: context.account.peerId, query: trimmedQuery, offset: "") + return context.engine.messages.requestChatContextResults(botId: peer.id, peerId: context.account.peerId, query: query, offset: "") |> map { results -> ChatContextResultCollection? in return results?.results } @@ -618,7 +615,7 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon } ) } else { - globalMusic = .single(nil) + globalMusic = .single([]) } } @@ -670,13 +667,22 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon } } - if let globalMusic, !globalMusic.isEmpty { - index = 0 - section += 1 - - entries.append(.header(title: presentationData.strings.Attachment_PublicMusic, section: section)) - for message in globalMusic { - entries.append(.file(index: index, message: message, section: section)) + if let globalMusic { + if !globalMusic.isEmpty { + index = 0 + section += 1 + + entries.append(.header(title: presentationData.strings.Attachment_PublicMusic, section: section)) + for message in globalMusic { + entries.append(.file(index: index, message: message, section: section)) + index += 1 + } + } + } else if messages.isEmpty { + entries.append(.header(title: " ", section: 0)) + var index: Int32 = 0 + for _ in 0 ..< 16 { + entries.append(.file(index: index, message: nil, section: 0)) index += 1 } } @@ -694,6 +700,8 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon } let previousSearchItems = Atomic<[AttachmentFileSearchEntry]?>(value: nil) + let previousHadSharedItems = Atomic(value: false) + let previousHadSavedItems = Atomic(value: false) let previousHadGlobalItems = Atomic(value: false) self.searchDisposable.set((combineLatest(searchQuery, foundItems, self.presentationDataPromise.get()) |> deliverOnMainQueue).startStrict(next: { [weak self] query, entries, presentationData in @@ -702,17 +710,30 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon updateActivity(false) let firstTime = previousEntries == nil + var hasSharedItems = false + var hasSavedItems = false var hasGlobalItems = false + if let entries { for entry in entries { - if case let .header(_, section) = entry, section == 2 { - hasGlobalItems = true + if case let .header(title, section) = entry, title != " " { + if section == 0 { + hasSharedItems = true + } + if section == 1 { + hasSavedItems = true + } + if section == 2 { + hasGlobalItems = true + } } } } + let hadSharedItems = previousHadSharedItems.swap(hasSharedItems) + let hadSavedItems = previousHadSavedItems.swap(hasSavedItems) let hadGlobalItems = previousHadGlobalItems.swap(hasGlobalItems) - let crossfade = hadGlobalItems != hasGlobalItems + let crossfade = hadSharedItems != hasSharedItems || hadSavedItems != hasSavedItems || hadGlobalItems != hasGlobalItems let transition = attachmentFileSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries ?? [], isSearching: entries != nil, isEmpty: entries?.isEmpty ?? false, query: query ?? "", context: context, presentationData: presentationData, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, interaction: interaction, mode: mode, crossfade: crossfade) strongSelf.enqueueTransition(transition, firstTime: firstTime) @@ -751,7 +772,8 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon } override public func searchTextUpdated(text: String) { - self.searchQuery.set(.single(!text.isEmpty ? text : nil)) + let trimmedText = text.trimmingCharacters(in: .whitespacesAndNewlines) + self.searchQuery.set(.single(!trimmedText.isEmpty ? trimmedText : nil)) } private func enqueueTransition(_ transition: AttachmentFileSearchContainerTransition, firstTime: Bool) { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 67b37e55f9..1c565ad880 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -6706,6 +6706,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID public let coverTimestamp: Double? public let options: MediaEditorResultPrivacy public let stickers: [TelegramMediaFile] + public let music: TelegramMediaFile? public let randomId: Int64 init() { @@ -6715,6 +6716,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID self.coverTimestamp = nil self.options = MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false, folderIds: []) self.stickers = [] + self.music = nil self.randomId = 0 } @@ -6725,6 +6727,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID coverTimestamp: Double? = nil, options: MediaEditorResultPrivacy = MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false, folderIds: []), stickers: [TelegramMediaFile] = [], + music: TelegramMediaFile? = nil, randomId: Int64 = 0 ) { self.media = media @@ -6733,6 +6736,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID self.coverTimestamp = coverTimestamp self.options = options self.stickers = stickers + self.music = music self.randomId = randomId } } @@ -7766,6 +7770,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID coverTimestamp: nil, options: MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false, folderIds: []), stickers: [], + music: nil, randomId: 0 )], { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorStoryCompletion.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorStoryCompletion.swift index 2777369e75..77b74fb29c 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorStoryCompletion.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorStoryCompletion.swift @@ -129,7 +129,7 @@ extension MediaEditorScreenImpl { if self.isEmbeddedEditor && !(hasAnyChanges || hasEntityChanges) { self.saveDraft(id: randomId, isEdit: true) - self.completion([MediaEditorScreenImpl.Result(media: nil, mediaAreas: [], caption: caption, coverTimestamp: mediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId)], { [weak self] finished in + self.completion([MediaEditorScreenImpl.Result(media: nil, mediaAreas: [], caption: caption, coverTimestamp: mediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, music: nil, randomId: randomId)], { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.dismiss() Queue.mainQueue().justDispatch { @@ -464,7 +464,7 @@ extension MediaEditorScreenImpl { return } Logger.shared.log("MediaEditor", "Completed with video \(videoResult)") - self.completion([MediaEditorScreenImpl.Result(media: .video(video: videoResult, coverImage: coverImage, values: values, duration: duration, dimensions: values.resultDimensions), mediaAreas: mediaAreas, caption: caption, coverTimestamp: values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId)], { [weak self] finished in + self.completion([MediaEditorScreenImpl.Result(media: .video(video: videoResult, coverImage: coverImage, values: values, duration: duration, dimensions: values.resultDimensions), mediaAreas: mediaAreas, caption: caption, coverTimestamp: values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, music: nil, randomId: randomId)], { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.dismiss() Queue.mainQueue().justDispatch { @@ -506,7 +506,7 @@ extension MediaEditorScreenImpl { return } Logger.shared.log("MediaEditor", "Completed with image \(resultImage)") - self.completion([MediaEditorScreenImpl.Result(media: .image(image: resultImage, dimensions: PixelDimensions(resultImage.size)), mediaAreas: mediaAreas, caption: caption, coverTimestamp: nil, options: self.state.privacy, stickers: stickers, randomId: randomId)], { [weak self] finished in + self.completion([MediaEditorScreenImpl.Result(media: .image(image: resultImage, dimensions: PixelDimensions(resultImage.size)), mediaAreas: mediaAreas, caption: caption, coverTimestamp: nil, options: self.state.privacy, stickers: stickers, music: nil, randomId: randomId)], { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.dismiss() Queue.mainQueue().justDispatch { @@ -683,6 +683,7 @@ extension MediaEditorScreenImpl { coverTimestamp: itemMediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, + music: nil, randomId: randomId ) completion(result) @@ -765,6 +766,7 @@ extension MediaEditorScreenImpl { coverTimestamp: nil, options: self.state.privacy, stickers: stickers, + music: nil, randomId: randomId ) completion(result) @@ -852,12 +854,11 @@ extension MediaEditorScreenImpl { coverTimestamp: nil, options: self.state.privacy, stickers: [], + music: nil, randomId: randomId ) } - - func updateMediaEditorEntities() { guard let mediaEditor = self.node.mediaEditor else { return diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index 8b0ea616d9..9bcaa14158 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -318,6 +318,7 @@ public final class StoryContentContextImpl: StoryContentContext { isMy: item.isMy, myReaction: item.myReaction, forwardInfo: forwardInfo, + music: item.music.flatMap(EngineMedia.init), author: item.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) }, folderIds: item.folderIds ) @@ -356,6 +357,7 @@ public final class StoryContentContextImpl: StoryContentContext { isMy: true, myReaction: nil, forwardInfo: pendingForwardsInfo[item.randomId], + music: item.music.flatMap(EngineMedia.init), author: nil, folderIds: item.folders )) @@ -1367,6 +1369,7 @@ public final class SingleStoryContentContextImpl: StoryContentContext { isMy: itemValue.isMy, myReaction: itemValue.myReaction, forwardInfo: forwardInfo, + music: itemValue.music.flatMap(EngineMedia.init), author: itemValue.authorId.flatMap { peers[$0].flatMap(EnginePeer.init) }, folderIds: itemValue.folderIds ) @@ -2308,6 +2311,7 @@ private func getCachedStory(storyId: StoryId, transaction: Transaction) -> Engin isMy: item.isMy, myReaction: item.myReaction, forwardInfo: item.forwardInfo.flatMap { EngineStoryItem.ForwardInfo($0, transaction: transaction) }, + music: item.music.flatMap(EngineMedia.init), author: item.authorId.flatMap { transaction.getPeer($0).flatMap(EnginePeer.init) }, folderIds: item.folderIds ) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift index d16cafd51c..c267b8633c 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift @@ -62,6 +62,7 @@ final class StoryContentCaptionComponent: Component { let author: EnginePeer let forwardInfo: EngineStoryItem.ForwardInfo? let forwardInfoStory: Signal? + let music: EngineMedia? let entities: [MessageTextEntity] let entityFiles: [EngineMedia.Id: TelegramMediaFile] let action: (Action) -> Void @@ -79,6 +80,7 @@ final class StoryContentCaptionComponent: Component { author: EnginePeer, forwardInfo: EngineStoryItem.ForwardInfo?, forwardInfoStory: Signal?, + music: EngineMedia?, entities: [MessageTextEntity], entityFiles: [EngineMedia.Id: TelegramMediaFile], action: @escaping (Action) -> Void, @@ -94,6 +96,7 @@ final class StoryContentCaptionComponent: Component { self.author = author self.forwardInfo = forwardInfo self.forwardInfoStory = forwardInfoStory + self.music = music self.text = text self.entities = entities self.entityFiles = entityFiles @@ -123,6 +126,9 @@ final class StoryContentCaptionComponent: Component { if lhs.forwardInfo != rhs.forwardInfo { return false } + if lhs.music != rhs.music { + return false + } if lhs.text != rhs.text { return false } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 9c4c730618..6cff603f75 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -4557,6 +4557,7 @@ public final class StoryItemSetContainerComponent: Component { author: component.slice.effectivePeer, forwardInfo: component.slice.item.storyItem.forwardInfo, forwardInfoStory: forwardInfoStory, + music: component.slice.item.storyItem.music, entities: enableEntities ? component.slice.item.storyItem.entities : [], entityFiles: component.slice.item.entityFiles, action: { [weak self] action in diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 735fc56e4e..131c7a4632 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -868,7 +868,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, itemOffsetInsets: itemOffsetInsets, duration: duration, curve: curve) self.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets) if let replacementHistoryNode = self.replacementHistoryNode { - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, duration: 0.0, curve: .Default(duration: nil)) + transition.updateFrame(node: replacementHistoryNode, frame: CGRect(origin: CGPoint(x: 0.0, y: listTopInset), size: listNodeSize)) + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, itemOffsetInsets: itemOffsetInsets, duration: 0.0, curve: .Default(duration: nil)) replacementHistoryNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets) } @@ -1091,13 +1092,13 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu let leftOverlayFrame = CGRect(origin: CGPoint(x: 0.0, y: topOverlayFrame.maxY - 1.0), size: CGSize(width: sideInset, height: layout.size.height)) let rightOverlayFrame = CGRect(origin: CGPoint(x: layout.size.width - sideInset, y: topOverlayFrame.maxY - 1.0), size: CGSize(width: sideInset, height: layout.size.height)) - self.historyFrameNode.frame = frameFrame + transition.updateFrame(node: self.historyFrameNode, frame: frameFrame) self.historyFrameTopOverlayClipNode.frame = topOverlayFrame self.historyFrameTopOverlayNode.frame = CGRect(origin: .zero, size: CGSize(width: topOverlayFrame.width, height: 78.0)) self.historyFrameLeftOverlayNode.frame = leftOverlayFrame self.historyFrameRightOverlayNode.frame = rightOverlayFrame if let image = self.historyFrameTopMaskNode.image { - self.historyFrameTopMaskNode.frame = CGRect(origin: CGPoint(x: sideInset, y: topOverlayFrame.maxY), size: CGSize(width: layout.size.width - sideInset * 2.0, height: image.size.height)) + self.historyFrameTopMaskNode.frame = CGRect(origin: CGPoint(x: sideInset, y: topOverlayFrame.maxY - UIScreenPixel), size: CGSize(width: layout.size.width - sideInset * 2.0, height: image.size.height)) } self.historyFrameTopMaskNode.isHidden = self.controlsNode.hasPlainBackground @@ -1157,23 +1158,33 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu historyNode.updateFloatingHeaderOffset = { [weak self] offset, _ in self?.replacementHistoryNodeFloatingOffset = offset } + self.replacementHistoryNodeFloatingOffset = nil self.replacementHistoryNode = historyNode if let layout = self.validLayout { let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top) + let controlsHeight = self.controlsNode.frame.height var insets = UIEdgeInsets() insets.left = 16.0 insets.right = 16.0 insets.bottom = 0.0 - - let listTopInset = layoutTopInset - let listNodeSize = CGSize(width: layout.size.width, height: layout.size.height - listTopInset) + + let headerHeight = self.effectiveHeaderHeight + let listTopInset = layoutTopInset + headerHeight + let listNodeSize = CGSize(width: layout.size.width, height: layout.size.height - listTopInset - controlsHeight) insets.top = max(0.0, listNodeSize.height - floor(62.0 * 3.5)) + var itemOffsetInsets = insets + if let playlistLocation = self.playlistLocation as? PeerMessagesPlaylistLocation, case let .savedMusic(_, _, canReorder) = playlistLocation, canReorder { + itemOffsetInsets.top = 0.0 + itemOffsetInsets.bottom = 0.0 + insets = itemOffsetInsets + } + historyNode.frame = CGRect(origin: CGPoint(x: 0.0, y: listTopInset), size: listNodeSize) - - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, duration: 0.0, curve: .Default(duration: nil)) + + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, itemOffsetInsets: itemOffsetInsets, duration: 0.0, curve: .Default(duration: nil)) historyNode.updateLayout(transition: .immediate, updateSizeAndInsets: updateSizeAndInsets) } self.replacementHistoryNodeReadyDisposable.set((historyNode.historyState.get() |> take(1) |> deliverOnMainQueue).startStrict(next: { [weak self] _ in @@ -1194,21 +1205,15 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu self.setupReordering() self.updateHistoryContentOffset(replacementHistoryNode.visibleContentOffset(), transition: .immediate) - if let validLayout = self.validLayout, let offset = self.replacementHistoryNodeFloatingOffset, let previousOffset = self.floatingHeaderOffset { + if let offset = self.replacementHistoryNodeFloatingOffset, let previousOffset = self.floatingHeaderOffset { let offsetDelta = offset - previousOffset - - let layoutTopInset: CGFloat = max(validLayout.statusBarHeight ?? 0.0, validLayout.safeInsets.top) - - let controlsBottomOffset = max(layoutTopInset, offset) - + let previousBackgroundNode = ASDisplayNode() previousBackgroundNode.isLayerBacked = true previousBackgroundNode.backgroundColor = self.historyBackgroundContentNode.backgroundColor self.contentNode.insertSubnode(previousBackgroundNode, belowSubnode: previousHistoryNode) previousBackgroundNode.frame = self.historyBackgroundNode.frame - previousBackgroundNode.layer.animateFrame(from: previousBackgroundNode.frame, to: CGRect(origin: CGPoint(x: 0.0, y: controlsBottomOffset), size: validLayout.size), duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - self.updateFloatingHeaderOffset(offset: offset, transition: .animated(duration: 0.4, curve: .spring)) previousHistoryNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousHistoryNode] _ in previousHistoryNode?.removeFromSupernode() @@ -1237,8 +1242,12 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu } switch strongSelf.historyNode.visibleContentOffset() { case let .known(value): - if value <= -10.0 { - strongSelf.requestDismiss() + if let playlistLocation = strongSelf.playlistLocation as? PeerMessagesPlaylistLocation, case let .savedMusic(_, _, canReorder) = playlistLocation, canReorder { + + } else { + if value <= -10.0 { + strongSelf.requestDismiss() + } } default: break @@ -1251,14 +1260,16 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu if let layout = self.validLayout { let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top) + let controlsHeight = self.controlsNode.frame.height var insets = UIEdgeInsets() insets.left = 16.0 insets.right = 16.0 insets.bottom = 0.0 - let listTopInset = layoutTopInset - let listNodeSize = CGSize(width: layout.size.width, height: layout.size.height - listTopInset) + let headerHeight = self.effectiveHeaderHeight + let listTopInset = layoutTopInset + headerHeight + let listNodeSize = CGSize(width: layout.size.width, height: layout.size.height - listTopInset - controlsHeight) insets.top = max(0.0, listNodeSize.height - floor(62.0 * 3.5)) @@ -1277,6 +1288,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu self.historyNode.recursivelyEnsureDisplaySynchronously(true) } + self.replacementHistoryNodeFloatingOffset = nil self.updateHistoryContentOffset(self.historyNode.visibleContentOffset(), transition: .immediate) } } diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 6b7e1e20f1..11cdee3266 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -770,6 +770,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon randomId: result.randomId, forwardInfo: forwardInfo, folders: folders, + music: result.music, uploadInfo: results.count > 1 ? StoryUploadInfo(groupingId: groupingId, index: index, total: Int32(results.count)) : nil ) |> deliverOnMainQueue).startStandalone(next: { stableId in From 21adc7d0618f29467ce5ed5fadbb8f72cada730d Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 30 Mar 2026 02:55:35 +0200 Subject: [PATCH 5/8] Various improvements --- .../Sources/ListMessageFileItemNode.swift | 7 +- .../MediaNavigationAccessoryHeaderNode.swift | 5 +- .../Network/FetchedMediaResource.swift | 5 + .../Messages/PendingStoryManager.swift | 7 +- .../Sources/MediaEditorValues.swift | 6 +- .../Sources/MediaEditorScreen.swift | 14 +- .../Sources/MediaEditorStoryCompletion.swift | 29 ++-- .../MediaNavigationAccessoryHeaderNode.swift | 5 +- .../Sources/PlainButtonComponent.swift | 6 + .../Sources/ForwardInfoPanelComponent.swift | 106 +++++++++++++ .../StoryContentCaptionComponent.swift | 87 +++++++++-- .../StoryItemSetContainerComponent.swift | 146 +++++++++++++++++- .../OverlayAudioPlayerControllerNode.swift | 2 +- .../Sources/PollResultsController.swift | 4 + 14 files changed, 388 insertions(+), 41 deletions(-) diff --git a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift index 4505aaf2dc..75d4055d15 100644 --- a/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageFileItemNode.swift @@ -524,7 +524,7 @@ public final class ListMessageFileItemNode: ListMessageNode { } if isExtracted { - strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: item.presentationData.theme.theme.list.plainBackgroundColor) + strongSelf.extractedBackgroundImageNode.image = generateStretchableFilledCircleImage(diameter: 28.0, color: item.presentationData.theme.theme.list.itemModalBlocksBackgroundColor) } if let extractedRect = strongSelf.extractedRect, let nonExtractedRect = strongSelf.nonExtractedRect { @@ -1036,7 +1036,10 @@ public final class ListMessageFileItemNode: ListMessageNode { reorderInset = sizeAndApply.0 } - let contentRightInset = rightInset + rightOffset + reorderInset + var contentRightInset = rightInset + rightOffset + reorderInset + if !item.isGlobalSearchResult { + contentRightInset += 16.0 + } let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) let dateText = stringForRelativeTimestamp(strings: item.presentationData.strings, relativeTimestamp: item.message?.timestamp ?? 0, relativeTo: timestamp, dateTimeFormat: item.presentationData.dateTimeFormat) diff --git a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift index 4cca7d51fe..e76f43d4a4 100644 --- a/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift +++ b/submodules/TelegramBaseController/Sources/MediaNavigationAccessoryHeaderNode.swift @@ -279,12 +279,10 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, ASScrollVi self.scrollNode.addSubnode(self.previousItemNode) self.scrollNode.addSubnode(self.nextItemNode) - self.addSubnode(self.closeButton) self.addSubnode(self.rateButton) self.addSubnode(self.accessibilityAreaNode) self.actionButton.addSubnode(self.playPauseIconNode) - self.addSubnode(self.actionButton) self.closeButton.addTarget(self, action: #selector(self.closeButtonPressed), forControlEvents: .touchUpInside) self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), forControlEvents: .touchUpInside) @@ -296,6 +294,9 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, ASScrollVi self.addSubnode(self.scrubbingNode) + self.addSubnode(self.actionButton) + self.addSubnode(self.closeButton) + self.addSubnode(self.separatorNode) self.actionButton.highligthedChanged = { [weak self] highlighted in diff --git a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift index 40fd07692b..553b840e4f 100644 --- a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift +++ b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift @@ -175,6 +175,11 @@ private func findMediaResource(media: Media, previousMedia: Media?, resource: Me return representation.resource } } + if let video = image.video { + if let resource = findMediaResource(media: video, previousMedia: previousMedia, resource: resource) { + return resource + } + } } else if let file = media as? TelegramMediaFile { if areResourcesEqual(file.resource, resource) { return file.resource diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift index 0859aeb5c7..fd0f176dc1 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift @@ -201,8 +201,11 @@ public extension Stories { self.folders = try container.decodeIfPresent([Int64].self, forKey: .folders) ?? [] - let musicData = try container.decode(Data.self, forKey: .music) - self.music = PostboxDecoder(buffer: MemoryBuffer(data: musicData)).decodeRootObject() as? TelegramMediaFile + if let musicData = try container.decodeIfPresent(Data.self, forKey: .music) { + self.music = PostboxDecoder(buffer: MemoryBuffer(data: musicData)).decodeRootObject() as? TelegramMediaFile + } else { + self.music = nil + } self.uploadInfo = try container.decodeIfPresent(StoryUploadInfo.self, forKey: .uploadInfo) } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift index 6fbf34e316..dc285429ae 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift @@ -66,23 +66,27 @@ public struct MediaAudioTrack: Codable, Equatable { case artist case title case duration + case file } public let path: String public let artist: String? public let title: String? public let duration: Double + public let file: TelegramMediaFile? public init( path: String, artist: String?, title: String?, - duration: Double + duration: Double, + file: TelegramMediaFile? = nil ) { self.path = path self.artist = artist self.title = title self.duration = duration + self.file = file } } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 1c565ad880..f9f3e01ef5 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -5163,7 +5163,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID return } - self.insertAudio(path: copyPath, fileName: fileName) + self.insertAudio(path: copyPath, fileName: fileName, file: file.media as? TelegramMediaFile) } }) } @@ -5224,7 +5224,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID }), in: .window(.root)) } - private func insertAudio(path: String, fileName: String, dispose: (() -> Void)? = nil) { + private func insertAudio(path: String, fileName: String, file: TelegramMediaFile? = nil, dispose: (() -> Void)? = nil) { guard let mediaEditor = self.mediaEditor else { return } @@ -5300,7 +5300,15 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID audioTrimRange = 0 ..< min(15, audioDuration) } - mediaEditor.setAudioTrack(MediaAudioTrack(path: fileName, artist: artist, title: title, duration: audioDuration), trimRange: audioTrimRange, offset: audioOffset) + var passFile = false + if let file { + for attribute in file.attributes { + if case let .Audio(_, _, title, _, _) = attribute, let title, !title.isEmpty { + passFile = true + } + } + } + mediaEditor.setAudioTrack(MediaAudioTrack(path: fileName, artist: artist, title: title, duration: audioDuration, file: passFile ? file : nil), trimRange: audioTrimRange, offset: audioOffset) mediaEditor.seek(mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, andPlay: true) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorStoryCompletion.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorStoryCompletion.swift index 77b74fb29c..0f51bd6e6a 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorStoryCompletion.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorStoryCompletion.swift @@ -129,7 +129,7 @@ extension MediaEditorScreenImpl { if self.isEmbeddedEditor && !(hasAnyChanges || hasEntityChanges) { self.saveDraft(id: randomId, isEdit: true) - self.completion([MediaEditorScreenImpl.Result(media: nil, mediaAreas: [], caption: caption, coverTimestamp: mediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, music: nil, randomId: randomId)], { [weak self] finished in + self.completion([MediaEditorScreenImpl.Result(media: nil, mediaAreas: [], caption: caption, coverTimestamp: mediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, music: mediaEditor.values.audioTrack?.file, randomId: randomId)], { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.dismiss() Queue.mainQueue().justDispatch { @@ -464,7 +464,7 @@ extension MediaEditorScreenImpl { return } Logger.shared.log("MediaEditor", "Completed with video \(videoResult)") - self.completion([MediaEditorScreenImpl.Result(media: .video(video: videoResult, coverImage: coverImage, values: values, duration: duration, dimensions: values.resultDimensions), mediaAreas: mediaAreas, caption: caption, coverTimestamp: values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, music: nil, randomId: randomId)], { [weak self] finished in + self.completion([MediaEditorScreenImpl.Result(media: .video(video: videoResult, coverImage: coverImage, values: values, duration: duration, dimensions: values.resultDimensions), mediaAreas: mediaAreas, caption: caption, coverTimestamp: values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, music: values.audioTrack?.file, randomId: randomId)], { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.dismiss() Queue.mainQueue().justDispatch { @@ -506,7 +506,7 @@ extension MediaEditorScreenImpl { return } Logger.shared.log("MediaEditor", "Completed with image \(resultImage)") - self.completion([MediaEditorScreenImpl.Result(media: .image(image: resultImage, dimensions: PixelDimensions(resultImage.size)), mediaAreas: mediaAreas, caption: caption, coverTimestamp: nil, options: self.state.privacy, stickers: stickers, music: nil, randomId: randomId)], { [weak self] finished in + self.completion([MediaEditorScreenImpl.Result(media: .image(image: resultImage, dimensions: PixelDimensions(resultImage.size)), mediaAreas: mediaAreas, caption: caption, coverTimestamp: nil, options: self.state.privacy, stickers: stickers, music: values.audioTrack?.file, randomId: randomId)], { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.dismiss() Queue.mainQueue().justDispatch { @@ -636,7 +636,7 @@ extension MediaEditorScreenImpl { } guard let avAsset else { Queue.mainQueue().async { - completion(self.createEmptyResult(randomId: randomId)) + completion(self.createEmptyResult(randomId: randomId, music: itemMediaEditor.values.audioTrack?.file)) } return } @@ -683,19 +683,19 @@ extension MediaEditorScreenImpl { coverTimestamp: itemMediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, - music: nil, + music: itemMediaEditor.values.audioTrack?.file, randomId: randomId ) completion(result) } else { - completion(self.createEmptyResult(randomId: randomId)) + completion(self.createEmptyResult(randomId: randomId, music: itemMediaEditor.values.audioTrack?.file)) } } } else { - completion(self.createEmptyResult(randomId: randomId)) + completion(self.createEmptyResult(randomId: randomId, music: itemMediaEditor.values.audioTrack?.file)) } } else { - completion(self.createEmptyResult(randomId: randomId)) + completion(self.createEmptyResult(randomId: randomId, music: itemMediaEditor.values.audioTrack?.file)) } } } @@ -737,7 +737,7 @@ extension MediaEditorScreenImpl { return } guard let image else { - completion(self.createEmptyResult(randomId: randomId)) + completion(self.createEmptyResult(randomId: randomId, music: itemMediaEditor.values.audioTrack?.file)) return } itemMediaEditor.replaceSource(image, additionalImage: nil, time: .zero, mirror: false) @@ -766,16 +766,16 @@ extension MediaEditorScreenImpl { coverTimestamp: nil, options: self.state.privacy, stickers: stickers, - music: nil, + music: itemMediaEditor.values.audioTrack?.file, randomId: randomId ) completion(result) } else { - completion(self.createEmptyResult(randomId: randomId)) + completion(self.createEmptyResult(randomId: randomId, music: itemMediaEditor.values.audioTrack?.file)) } } } else { - completion(self.createEmptyResult(randomId: randomId)) + completion(self.createEmptyResult(randomId: randomId, music: itemMediaEditor.values.audioTrack?.file)) } } @@ -818,7 +818,6 @@ extension MediaEditorScreenImpl { mode: .default, subject: editorSubject, values: values, - hasHistogram: false, isStandalone: true ) } @@ -842,7 +841,7 @@ extension MediaEditorScreenImpl { } } - private func createEmptyResult(randomId: Int64) -> MediaEditorScreenImpl.Result { + private func createEmptyResult(randomId: Int64, music: TelegramMediaFile? = nil) -> MediaEditorScreenImpl.Result { let emptyImage = UIImage() return MediaEditorScreenImpl.Result( media: .image( @@ -854,7 +853,7 @@ extension MediaEditorScreenImpl { coverTimestamp: nil, options: self.state.privacy, stickers: [], - music: nil, + music: music, randomId: randomId ) } diff --git a/submodules/TelegramUI/Components/MediaPlaybackHeaderPanelComponent/Sources/MediaNavigationAccessoryHeaderNode.swift b/submodules/TelegramUI/Components/MediaPlaybackHeaderPanelComponent/Sources/MediaNavigationAccessoryHeaderNode.swift index 2760ca964b..21b4d08ef8 100644 --- a/submodules/TelegramUI/Components/MediaPlaybackHeaderPanelComponent/Sources/MediaNavigationAccessoryHeaderNode.swift +++ b/submodules/TelegramUI/Components/MediaPlaybackHeaderPanelComponent/Sources/MediaNavigationAccessoryHeaderNode.swift @@ -280,12 +280,10 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, ASScrollVi self.scrollNode.addSubnode(self.previousItemNode) self.scrollNode.addSubnode(self.nextItemNode) - self.addSubnode(self.closeButton) self.addSubnode(self.rateButton) self.addSubnode(self.accessibilityAreaNode) self.actionButton.addSubnode(self.playPauseIconNode) - self.addSubnode(self.actionButton) self.closeButton.addTarget(self, action: #selector(self.closeButtonPressed), forControlEvents: .touchUpInside) self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), forControlEvents: .touchUpInside) @@ -297,6 +295,9 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, ASScrollVi self.addSubnode(self.scrubbingNode) + self.addSubnode(self.actionButton) + self.addSubnode(self.closeButton) + self.actionButton.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { diff --git a/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift b/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift index fadcc5c0d6..02cf87e2bb 100644 --- a/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift +++ b/submodules/TelegramUI/Components/PlainButtonComponent/Sources/PlainButtonComponent.swift @@ -20,6 +20,7 @@ public final class PlainButtonComponent: Component { public let animateAlpha: Bool public let animateScale: Bool public let animateContents: Bool + public let rasterizeOnScaleAnimation: Bool public let tag: AnyObject? public init( @@ -33,6 +34,7 @@ public final class PlainButtonComponent: Component { animateAlpha: Bool = true, animateScale: Bool = true, animateContents: Bool = true, + rasterizeOnScaleAnimation: Bool = true, tag: AnyObject? = nil ) { self.content = content @@ -45,6 +47,7 @@ public final class PlainButtonComponent: Component { self.animateAlpha = animateAlpha self.animateScale = animateScale self.animateContents = animateContents + self.rasterizeOnScaleAnimation = rasterizeOnScaleAnimation self.tag = tag } @@ -76,6 +79,9 @@ public final class PlainButtonComponent: Component { if lhs.animateContents != rhs.animateContents { return false } + if lhs.rasterizeOnScaleAnimation != rhs.rasterizeOnScaleAnimation { + return false + } if lhs.tag !== rhs.tag { return false } diff --git a/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/Sources/ForwardInfoPanelComponent.swift b/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/Sources/ForwardInfoPanelComponent.swift index 6cf95f03eb..d8c58d4059 100644 --- a/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/Sources/ForwardInfoPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent/Sources/ForwardInfoPanelComponent.swift @@ -206,3 +206,109 @@ public final class ForwardInfoPanelComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } + +public final class MusicPanelComponent: Component { + public let context: AccountContext + public let file: TelegramMediaFile + + public init( + context: AccountContext, + file: TelegramMediaFile + ) { + self.context = context + self.file = file + } + + public static func ==(lhs: MusicPanelComponent, rhs: MusicPanelComponent) -> Bool { + if lhs.file != rhs.file { + return false + } + return true + } + + public final class View: UIView { + public let backgroundView: UIImageView + private let blurBackgroundView: UIVisualEffectView + private var iconView = UIImageView() + private var title = ComponentView() + private var text = ComponentView() + + private var component: MusicPanelComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + if #available(iOS 13.0, *) { + self.blurBackgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterialDark)) + } else { + self.blurBackgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) + } + self.blurBackgroundView.clipsToBounds = true + + self.backgroundView = UIImageView() + self.backgroundView.image = generateStretchableFilledCircleImage(radius: 4.0, color: UIColor(white: 0.0, alpha: 0.4)) + + super.init(frame: frame) + + self.iconView.image = UIImage(bundleImageName: "Media Editor/SmallAudio") + self.addSubview(self.iconView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: MusicPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.state = state + + var titleOffset: CGFloat = 0.0 + if let image = self.iconView.image { + self.iconView.frame = CGRect(origin: CGPoint(x: 10.0, y: 5.0), size: image.size) + } + titleOffset += 29.0 + + let text = NSMutableAttributedString() + for attribute in component.file.attributes { + if case let .Audio(_, _, title, performer, _) = attribute, let title, !title.isEmpty { + text.append(NSAttributedString(string: title, font: Font.semibold(13.0), textColor: .white)) + if let performer, !performer.isEmpty { + text.append(NSAttributedString(string: " • ", font: Font.semibold(13.0), textColor: .white.withAlphaComponent(0.28))) + text.append(NSAttributedString(string: performer, font: Font.regular(13.0), textColor: .white)) + } + } + } + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(text), + maximumNumberOfLines: 1 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - titleOffset - 20.0, height: availableSize.height) + ) + let titleFrame = CGRect(origin: CGPoint(x: titleOffset, y: 5.0 - UIScreenPixel), size: titleSize) + if let view = self.title.view { + if view.superview == nil { + self.addSubview(view) + } + view.frame = titleFrame + } + + let size = CGSize(width: titleSize.width + 39.0, height: 26.0) + self.blurBackgroundView.frame = CGRect(origin: .zero, size: size) + self.insertSubview(self.blurBackgroundView, at: 0) + self.blurBackgroundView.layer.cornerRadius = size.height / 2.0 + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift index c267b8633c..49d6030c89 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift @@ -70,6 +70,7 @@ final class StoryContentCaptionComponent: Component { let textSelectionAction: (NSAttributedString, TextSelectionAction) -> Void let controller: () -> ViewController? let openStory: (EnginePeer, EngineStoryItem?) -> Void + let openMusic: (FileMediaReference, UIView) -> Void init( externalState: ExternalState, @@ -87,7 +88,8 @@ final class StoryContentCaptionComponent: Component { longTapAction: @escaping (Action) -> Void, textSelectionAction: @escaping (NSAttributedString, TextSelectionAction) -> Void, controller: @escaping () -> ViewController?, - openStory: @escaping (EnginePeer, EngineStoryItem?) -> Void + openStory: @escaping (EnginePeer, EngineStoryItem?) -> Void, + openMusic: @escaping (FileMediaReference, UIView) -> Void ) { self.externalState = externalState self.context = context @@ -105,6 +107,7 @@ final class StoryContentCaptionComponent: Component { self.textSelectionAction = textSelectionAction self.controller = controller self.openStory = openStory + self.openMusic = openMusic } static func ==(lhs: StoryContentCaptionComponent, rhs: StoryContentCaptionComponent) -> Bool { @@ -187,6 +190,8 @@ final class StoryContentCaptionComponent: Component { private let scrollBottomFullMaskView: UIView private let scrollTopMaskView: UIImageView + private var musicPanel: ComponentView? + private var forwardInfoPanel: ComponentView? private var forwardInfoDisposable: Disposable? private var forwardInfoStory: EngineStoryItem? @@ -316,10 +321,10 @@ final class StoryContentCaptionComponent: Component { let contentItem = self.isExpanded ? self.expandedText : self.collapsedText - if let textView = contentItem.textNode?.textNode.view { - let textLocalPoint = self.convert(point, to: textView) - if textLocalPoint.y >= -7.0 { - return self.textSelectionNode?.view ?? textView + if let musicView = self.musicPanel?.view { + let musicLocalPoint = self.convert(point, to: musicView) + if let result = musicView.hitTest(musicLocalPoint, with: nil) { + return result } } @@ -330,6 +335,13 @@ final class StoryContentCaptionComponent: Component { } } + if let textView = contentItem.textNode?.textNode.view { + let textLocalPoint = self.convert(point, to: textView) + if textLocalPoint.y >= -7.0 { + return self.textSelectionNode?.view ?? textView + } + } + return nil } @@ -617,7 +629,8 @@ final class StoryContentCaptionComponent: Component { self.state = state let sideInset: CGFloat = 16.0 - let verticalInset: CGFloat = 7.0 + let verticalInset: CGFloat = 8.0 + let bottomPanelSpacing: CGFloat = 5.0 let textContainerSize = CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height - verticalInset * 2.0) var baseQuoteSecondaryTintColor: UIColor? @@ -716,6 +729,57 @@ final class StoryContentCaptionComponent: Component { let visibleTextHeight = collapsedTextLayout.0.size.height - textInsets.top - textInsets.bottom let textOverflowHeight: CGFloat = expandedTextLayout.0.size.height - textInsets.top - textInsets.bottom - visibleTextHeight let scrollContentSize = CGSize(width: availableSize.width, height: availableSize.height + textOverflowHeight) + let hasBottomStackContent = !component.text.isEmpty || component.forwardInfo != nil + var bottomContentOffset: CGFloat = 0.0 + + if let music = component.music?._asMedia() as? TelegramMediaFile { + let musicPanel: ComponentView + if let current = self.musicPanel { + musicPanel = current + } else { + musicPanel = ComponentView() + self.musicPanel = musicPanel + } + + let musicPanelSize = musicPanel.update( + transition: .immediate, + component: AnyComponent( + PlainButtonComponent( + content: AnyComponent( + MusicPanelComponent( + context: component.context, + file: music + ) + ), + action: { [weak self] in + guard let self else { + return + } + if let sourceView = self.musicPanel?.view { + self.component?.openMusic(.standalone(media: music), sourceView) + } + }, + animateScale: false + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) + ) + bottomContentOffset = musicPanelSize.height + (hasBottomStackContent ? bottomPanelSpacing : 0.0) + let musicPanelFrame = CGRect(origin: CGPoint(x: sideInset - 8.0, y: availableSize.height - verticalInset - musicPanelSize.height), size: musicPanelSize) + if let view = musicPanel.view { + if view.superview == nil { + self.scrollView.addSubview(view) + transition.animateAlpha(view: view, from: 0.0, to: 1.0) + } + view.frame = musicPanelFrame + } + } else if let musicPanel = self.musicPanel { + self.musicPanel = nil + musicPanel.view?.removeFromSuperview() + } + + let contentBottomY = availableSize.height - verticalInset - bottomContentOffset if let forwardInfo = component.forwardInfo { let authorName: String @@ -778,8 +842,6 @@ final class StoryContentCaptionComponent: Component { fillsWidth: false ) ), - effectAlignment: .center, - minSize: nil, action: { [weak self] in if let self, case let .known(peer, _, _) = forwardInfo { self.component?.openStory(peer, self.forwardInfoStory) @@ -792,13 +854,14 @@ final class StoryContentCaptionComponent: Component { return nil })) } - } + }, + animateScale: false ) ), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) ) - let forwardInfoPanelFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - visibleTextHeight - verticalInset - forwardInfoPanelSize.height - 10.0), size: forwardInfoPanelSize) + let forwardInfoPanelFrame = CGRect(origin: CGPoint(x: sideInset, y: contentBottomY - visibleTextHeight - forwardInfoPanelSize.height - bottomPanelSpacing), size: forwardInfoPanelSize) if let view = forwardInfoPanel.view { if view.superview == nil { self.scrollView.addSubview(view) @@ -813,8 +876,8 @@ final class StoryContentCaptionComponent: Component { } - let collapsedTextFrame = CGRect(origin: CGPoint(x: sideInset - textInsets.left, y: availableSize.height - visibleTextHeight - verticalInset - textInsets.top), size: collapsedTextLayout.0.size) - let expandedTextFrame = CGRect(origin: CGPoint(x: sideInset - textInsets.left, y: availableSize.height - visibleTextHeight - verticalInset - textInsets.top), size: expandedTextLayout.0.size) + let collapsedTextFrame = CGRect(origin: CGPoint(x: sideInset - textInsets.left, y: contentBottomY - visibleTextHeight - textInsets.top), size: collapsedTextLayout.0.size) + let expandedTextFrame = CGRect(origin: CGPoint(x: sideInset - textInsets.left, y: contentBottomY - visibleTextHeight - textInsets.top), size: expandedTextLayout.0.size) var spoilerExpandRect: CGRect? if let location = self.displayContentsUnderSpoilers.location { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 6cff603f75..e1b6ffa7c0 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -4523,7 +4523,7 @@ public final class StoryItemSetContainerComponent: Component { } } - if !isUnsupported, !component.slice.item.storyItem.text.isEmpty || component.slice.item.storyItem.forwardInfo != nil { + if !isUnsupported, !component.slice.item.storyItem.text.isEmpty || component.slice.item.storyItem.forwardInfo != nil || component.slice.item.storyItem.music != nil { var captionItemTransition = transition let captionItem: CaptionItem if let current = self.captionItem { @@ -4709,6 +4709,12 @@ public final class StoryItemSetContainerComponent: Component { } } } + }, + openMusic: { [weak self] file, sourceView in + guard let self else { + return + } + self.performMusicAction(file: file, sourceView: sourceView, gesture: nil) } )), environment: {}, @@ -7541,6 +7547,144 @@ public final class StoryItemSetContainerComponent: Component { }) } + private func performMusicAction(file: FileMediaReference, sourceView: UIView, gesture: ContextGesture?) { + guard let component = self.component, let controller = component.controller() else { + return + } + + self.dismissAllTooltips() + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) + + let items = component.context.engine.peers.savedMusicIds() + |> take(1) + |> map { [weak self] savedIds -> ContextController.Items in + var items: [ContextMenuItem] = [] + + items.append( + .action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveTo, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/DownloadTone"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in + if let self { + var subActions: [ContextMenuItem] = [] +// subActions.append( +// .action(ContextMenuActionItem(text: presentationData.strings.Common_Back, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor) }, iconPosition: .left, action: { c, _ in +// c?.popItems() +// })) +// ) +// subActions.append(.separator) + + subActions.append( + .action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveTo_Profile, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + + guard let self, let component = self.component else { + return + } + + let _ = component.context.engine.peers.addSavedMusic(file: file).start() + + guard let controller = component.controller() as? StoryContainerScreen else { + return + } + + let overlayController = UndoOverlayController( + presentationData: presentationData, + content: .universalImage( + image: generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SavedMusic"), color: .white)!, + size: nil, + title: nil, + text: presentationData.strings.MediaPlayer_SavedMusic_AddedToProfile, + customUndoText: presentationData.strings.MediaPlayer_SavedMusic_AddedToProfile_View, + timeout: 3.0 + ), + action: { [weak self] action in + guard let self, let component = self.component, case .undo = action else { + return false + } + let _ = (component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: component.context.account.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let component = self.component, let peer else { + return + } + guard let controller = component.controller() as? StoryContainerScreen else { + return + } + guard let navigationController = controller.navigationController as? NavigationController else { + return + } + if let controller = component.context.sharedContext.makePeerInfoController( + context: component.context, + updatedPresentationData: nil, + peer: peer._asPeer(), + mode: .myProfile, + avatarInitiallyExpanded: false, + fromChat: false, + requestsContext: nil + ) { + navigationController.pushViewController(controller) + } + }) + return true + } + ) + controller.present(overlayController, in: .current) + })) + ) + + subActions.append( + .action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveTo_SavedMessages, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in + f(.default) + + guard let self, let component = self.component else { + return + } + + let _ = component.context.engine.messages.enqueueOutgoingMessage(to: component.context.account.peerId, replyTo: nil, content: .file(file)).start() + + guard let controller = component.controller() as? StoryContainerScreen else { + return + } + + let overlayController = UndoOverlayController( + presentationData: presentationData, + content: .forward(savedMessages: true, text: presentationData.strings.MediaPlayer_AudioForwardedToSavedMesagesTooltip), + action: { _ in + return true + } + ) + controller.present(overlayController, in: .current) + })) + ) + + let noAction: ((ContextMenuActionItem.Action) -> Void)? = nil + subActions.append( + .action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveTo_Info, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: noAction)) + ) + + c?.pushItems(items: .single(ContextController.Items(content: .list(subActions)))) + } + })) + ) + return ContextController.Items(content: .list(items)) + } + + let contextController = makeContextController( + presentationData: presentationData, + source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView, position: .top)), + items: items, + gesture: gesture + ) + contextController.dismissed = { [weak self] in + guard let self else { + return + } + self.contextController = nil + self.updateIsProgressPaused() + } + self.contextController = contextController + self.updateIsProgressPaused() + controller.present(contextController, in: .window(.root)) + } + private func beginPictureInPicture() { guard let component = self.component, let visibleItem = self.visibleItems[component.slice.item.id] else { return diff --git a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift index 131c7a4632..1324d32411 100644 --- a/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift +++ b/submodules/TelegramUI/Sources/OverlayAudioPlayerControllerNode.swift @@ -963,7 +963,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu self.updateHistoryContentOffset(self.historyNode.visibleContentOffset(), transition: transition) var layout = layout - layout.intrinsicInsets.bottom = controlsHeight + 8.0 + layout.intrinsicInsets.bottom = controlsHeight + (self.historyNode.hasAnyMessages ? 0.0 : 8.0) self.getParentController()?.presentationContext.containerLayoutUpdated(layout, transition: transition) } diff --git a/submodules/TelegramUI/Sources/PollResultsController.swift b/submodules/TelegramUI/Sources/PollResultsController.swift index 7650c459da..fb9234290f 100644 --- a/submodules/TelegramUI/Sources/PollResultsController.swift +++ b/submodules/TelegramUI/Sources/PollResultsController.swift @@ -464,6 +464,10 @@ public func pollResultsController(context: AccountContext, messageId: EngineMess resultsContext.state ) |> map { presentationData, state, resultsState -> (ItemListControllerState, (ItemListNodeState, Any)) in + var presentationData = presentationData + let updatedTheme = presentationData.theme.withModalBlocksBackground() + presentationData = presentationData.withUpdated(theme: updatedTheme) + var isEmpty = false for (_, optionState) in resultsState.options { if !optionState.hasLoadedOnce { From 8f8dca5795a3f5664d19623be9911bb65cdedfcf Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 30 Mar 2026 03:01:36 +0200 Subject: [PATCH 6/8] Allow attach menu apps in paid message chats --- .../ChatControllerOpenAttachmentMenu.swift | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index b63e67f68b..bf77a22fe9 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -188,26 +188,26 @@ extension ChatControllerImpl { break } - if !isPaidMessages { - for bot in attachMenuBots.reversed() { - var peerType = peerType - if bot.peer.id == peer.id { - peerType.insert(.sameBot) - peerType.remove(.bot) - } - let button: AttachmentButtonType = .app(bot) - if !bot.peerTypes.intersection(peerType).isEmpty { - buttons.insert(button, at: 1) - - if case let .bot(botId, _, _) = subject { - if initialButton == nil && bot.peer.id == botId { - initialButton = button - } + for bot in attachMenuBots.reversed() { + var peerType = peerType + if bot.peer.id == peer.id { + peerType.insert(.sameBot) + peerType.remove(.bot) + } + let button: AttachmentButtonType = .app(bot) + if !bot.peerTypes.intersection(peerType).isEmpty { + buttons.insert(button, at: 1) + + if case let .bot(botId, _, _) = subject { + if initialButton == nil && bot.peer.id == botId { + initialButton = button } } - allButtons.insert(button, at: 1) } + allButtons.insert(button, at: 1) + } + if !isPaidMessages { if context.isPremium, shortcutMessageList.items.count > 0, let user = peer as? TelegramUser, user.botInfo == nil { if let index = buttons.firstIndex(where: { $0 == .location }) { buttons.insert(.quickReply, at: index + 1) From ca3eacda45a45806bdd0b998d8121b17a1b73e71 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 30 Mar 2026 04:37:05 +0200 Subject: [PATCH 7/8] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 2 + .../GalleryUI/Sources/GalleryPagerNode.swift | 16 +- .../Items/UniversalVideoGalleryItem.swift | 190 ++++++++++++++---- .../Sources/LivePhotoButton.swift | 2 +- .../LivePhotoPlay.imageset/Contents.json | 12 ++ .../LivePhotoPlay.imageset/play1.pdf | Bin 0 -> 9018 bytes .../LivePhotoPlaying.imageset/Contents.json | 12 ++ .../LivePhotoPlaying.imageset/play2.pdf | Bin 0 -> 9395 bytes 8 files changed, 197 insertions(+), 37 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Media Gallery/LivePhotoPlay.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Gallery/LivePhotoPlay.imageset/play1.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Media Gallery/LivePhotoPlaying.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Media Gallery/LivePhotoPlaying.imageset/play2.pdf diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 3c73dfa600..73d3543c70 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -16166,3 +16166,5 @@ Error: %8$@"; "Attachment.PublicMusic" = "PUBLIC CHATS"; "PeerInfo.UnofficialSecurityRisk" = "%@ uses an unofficial Telegram client – messages to this user may be less secure."; + +"Gallery.Live" = "LIVE"; diff --git a/submodules/GalleryUI/Sources/GalleryPagerNode.swift b/submodules/GalleryUI/Sources/GalleryPagerNode.swift index 8ec3af00ca..af98d0480d 100644 --- a/submodules/GalleryUI/Sources/GalleryPagerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryPagerNode.swift @@ -128,6 +128,20 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest private var pagingEnabledDisposable: Disposable? private var edgeLongTapTimer: Foundation.Timer? + + private func isInteractiveControlView(_ view: UIView?) -> Bool { + var currentView = view + while let view = currentView { + if view is UIControl { + return true + } + if let _ = view.asyncdisplaykit_node as? ASButtonNode { + return true + } + currentView = view.superview + } + return false + } public init(pageGap: CGFloat, disableTapNavigation: Bool) { self.pageGap = pageGap @@ -220,7 +234,7 @@ public final class GalleryPagerNode: ASDisplayNode, ASScrollViewDelegate, ASGest return .fail } - if let result = strongSelf.hitTest(point, with: nil), let _ = result.asyncdisplaykit_node as? ASButtonNode { + if strongSelf.isInteractiveControlView(strongSelf.hitTest(point, with: nil)) { return .fail } diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 530f02b2f9..3077bd4ee4 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -38,6 +38,7 @@ import MultilineTextComponent import BundleIconComponent import VideoPlaybackControlsComponent import PhotoResources +import GlassBackgroundComponent public enum UniversalVideoGalleryItemContentInfo { case message(Message, GalleryMediaSubject?) @@ -893,7 +894,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private let statusButtonNode: HighlightableButtonNode private let statusNode: RadialStatusNode private var statusNodeShouldBeHidden = true - private var livePhotoIconNode: ASImageNode? + private var livePhotoButton: LivePhotoButton? private let playbackControls = ComponentView() @@ -963,9 +964,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private var activeEdgeRateIndicator: ComponentView? private var isAnimatingOut: Bool = false + private var isLivePhoto = false private var didAutoplayLivePhotoOnce = false - private var isPlayingLivePhotoByHolding = false + private var isLivePhotoPlaybackActive = false + private var isLivePhotoGestureActive = false init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) { self.context = context @@ -1275,6 +1278,14 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { let transition = ComponentTransition(transition) transition.setFrame(view: playbackControlsView, frame: playbackControlsFrame) } + + if let livePhotoButton = self.livePhotoButton { + let livePhotoButtonSize = livePhotoButton.update(isPlaying: self.isLivePhotoPlaybackActive) + if livePhotoButton.superview == nil { + self.view.addSubview(livePhotoButton) + } + transition.updateFrame(view: livePhotoButton, frame: CGRect(origin: CGPoint(x: 16.0, y: max(navigationBarHeight, layout.statusBarHeight ?? 0.0) + 10.0), size: livePhotoButtonSize)) + } if let pictureInPictureNode = self.pictureInPictureNode { if let item = self.item { @@ -1374,10 +1385,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.statusButtonNode.isHidden = true } + self.resetLivePhotoPlayback() self.dismissOnOrientationChange = item.landscape self.isLivePhoto = false self.didAutoplayLivePhotoOnce = false - self.isPlayingLivePhotoByHolding = false + self.isLivePhotoPlaybackActive = false var hasLinkedStickers = false if let content = item.content as? NativeVideoContent { @@ -1716,7 +1728,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { initialBuffering = true } isPaused = !whilePlaying - if strongSelf.isPlayingLivePhotoByHolding { + if strongSelf.isLivePhotoPlaybackActive { isPaused = false } var isStreaming = false @@ -1961,7 +1973,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } if strongSelf.isLivePhoto { - if !strongSelf.isPlayingLivePhotoByHolding { + if !strongSelf.isLivePhotoPlaybackActive { strongSelf.setLivePhotoVideoVisible(false, animated: true) } } else if let snapshotView = videoNode?.view.snapshotView(afterScreenUpdates: false) { @@ -1973,7 +1985,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if !strongSelf.isLivePhoto { videoNode?.seek(0.0) - } else if strongSelf.isPlayingLivePhotoByHolding { + } else if strongSelf.isLivePhotoPlaybackActive { videoNode?.playOnceWithSound(playAndRecord: false, actionAtEnd: .loop) } @@ -2059,22 +2071,29 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } if self.isLivePhoto { - let livePhotoIconNode: ASImageNode - if let current = self.livePhotoIconNode { - livePhotoIconNode = current + let livePhotoButton: LivePhotoButton + if let current = self.livePhotoButton { + livePhotoButton = current } else { - livePhotoIconNode = ASImageNode() - livePhotoIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/LiveOn"), color: .white) - //self.addSubnode(livePhotoIconNode) - self.livePhotoIconNode = livePhotoIconNode + livePhotoButton = LivePhotoButton(context: self.context) + livePhotoButton.pressed = { [weak self] in + guard let self else { + return + } + self.updateLivePhotoPlayback(isActive: !self.isLivePhotoPlaybackActive, animated: true) + } + self.livePhotoButton = livePhotoButton + self.view.addSubview(livePhotoButton) } - - if let icon = livePhotoIconNode.image { - livePhotoIconNode.frame = CGRect(origin: CGPoint(x: 8.0, y: 8.0), size: icon.size) - } - } else if let livePhotoIconNode = self.livePhotoIconNode { - self.livePhotoIconNode = nil - livePhotoIconNode.removeFromSupernode() +// if livePhotoButton.superview == nil { +// +// } +// if let validLayout = self.validLayout { +// self.containerLayoutUpdated(validLayout.layout, navigationBarHeight: validLayout.navigationBarHeight, transition: .immediate) +// } + } else if let livePhotoButton = self.livePhotoButton { + self.livePhotoButton = nil + livePhotoButton.removeFromSuperview() } } @@ -2169,24 +2188,29 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } - private func updateLivePhotoHoldPlayback(isActive: Bool, animated: Bool) { - guard self.isLivePhoto, let videoNode = self.videoNode else { + private func updateLivePhotoPlayback(isActive: Bool, animated: Bool) { + guard self.isLivePhoto, self.isLivePhotoPlaybackActive != isActive, let videoNode = self.videoNode else { return } - guard self.isPlayingLivePhotoByHolding != isActive else { - return - } - - self.isPlayingLivePhotoByHolding = isActive + + self.isLivePhotoPlaybackActive = isActive if isActive { self.setLivePhotoVideoVisible(true, animated: animated) videoNode.seek(0.0) - videoNode.playOnceWithSound(playAndRecord: false) + videoNode.playOnceWithSound(playAndRecord: false, actionAtEnd: .loop) } else { videoNode.pause() videoNode.seek(0.0) self.setLivePhotoVideoVisible(false, animated: animated) } + + if let validLayout = self.validLayout { + self.containerLayoutUpdated(validLayout.layout, navigationBarHeight: validLayout.navigationBarHeight, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate) + } + } + + private func resetLivePhotoPlayback() { + self.updateLivePhotoPlayback(isActive: false, animated: false) } override func centralityUpdated(isCentral: Bool) { @@ -2228,7 +2252,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } } } else { - self.updateLivePhotoHoldPlayback(isActive: false, animated: false) + self.resetLivePhotoPlayback() self.isPlayingPromise.set(false) self.isPlaying = false @@ -2265,17 +2289,19 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { if self.skipInitialPause { self.skipInitialPause = false } else { - self.updateLivePhotoHoldPlayback(isActive: false, animated: false) + self.resetLivePhotoPlayback() self.ignorePauseStatus = true videoNode.pause() videoNode.seek(0.0) } } else { - self.updateLivePhotoHoldPlayback(isActive: false, animated: false) + self.resetLivePhotoPlayback() if let status = self.playerStatusValue { self.maybeStorePlaybackStatus(status: status) } - videoNode.continuePlayingWithoutSound() + if !self.isLivePhoto { + videoNode.continuePlayingWithoutSound() + } } self.updateDisplayPlaceholder() } else if !item.fromPlayingVideo { @@ -2382,7 +2408,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private var actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd { if self.isLivePhoto { - return self.isPlayingLivePhotoByHolding ? .loop : .stop + return self.isLivePhotoPlaybackActive ? .loop : .stop } if let item = self.item { if !item.isSecret, let content = item.content as? NativeVideoContent, content.duration <= 30 { @@ -4082,7 +4108,13 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { return } if let edge, case .middle = edge { - self.updateLivePhotoHoldPlayback(isActive: true, animated: true) + guard self.isLivePhoto else { + return + } + if !self.isLivePhotoPlaybackActive { + self.updateLivePhotoPlayback(isActive: true, animated: true) + } + self.isLivePhotoGestureActive = true } else if let edge, case .right = edge { let effectiveRate: Double if let current = self.activeEdgeRateState { @@ -4097,7 +4129,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } videoNode.setBaseRate(effectiveRate) } else { - self.updateLivePhotoHoldPlayback(isActive: false, animated: true) + if self.isLivePhotoGestureActive { + self.updateLivePhotoPlayback(isActive: false, animated: true) + } + self.isLivePhotoGestureActive = false + if let (initialRate, _) = self.activeEdgeRateState { self.activeEdgeRateState = nil videoNode.setBaseRate(initialRate) @@ -4154,3 +4190,87 @@ final class HeaderContextReferenceContentSource: ContextReferenceContentSource { private func normalizeValue(_ value: CGFloat) -> CGFloat { return round(value * 10.0) / 10.0 } + +private final class LivePhotoButton: UIView { + private let context: AccountContext + + private let backgroundView: GlassBackgroundView + private let icon = ComponentView() + private let label = ComponentView() + private let button = HighlightTrackingButton() + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + return self.bounds.insetBy(dx: -16.0, dy: -16.0).contains(point) + } + + var pressed: () -> Void = {} + + init(context: AccountContext) { + self.context = context + + self.backgroundView = GlassBackgroundView() + + super.init(frame: .zero) + + self.addSubview(self.backgroundView) + self.backgroundView.contentView.addSubview(self.button) + + self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + @objc private func buttonPressed() { + self.pressed() + } + + func update(isPlaying: Bool = false) -> CGSize { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + + let iconName: String = isPlaying ? "Media Gallery/LivePhotoPlaying" : "Media Gallery/LivePhotoPlay" + let labelText: String = presentationData.strings.Gallery_Live + + let iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent( + BundleIconComponent(name: iconName, tintColor: .white) + ), + environment: {}, + containerSize: CGSize(width: 18.0, height: 18.0) + ) + let iconFrame = CGRect(origin: CGPoint(x: 8.0, y: floorToScreenPixels((30.0 - iconSize.height) / 2.0)), size: iconSize) + if let iconView = self.icon.view { + if iconView.superview == nil { + iconView.isUserInteractionEnabled = false + self.backgroundView.contentView.addSubview(iconView) + } + iconView.frame = iconFrame + } + + let labelSize = self.label.update( + transition: .immediate, + component: AnyComponent( + Text(text: labelText.uppercased(), font: Font.regular(12.0), color: .white) + ), + environment: {}, + containerSize: CGSize(width: 200.0, height: 18.0) + ) + let labelFrame = CGRect(origin: CGPoint(x: 28.0, y: floorToScreenPixels((30.0 - labelSize.height) / 2.0)), size: labelSize) + if let labelView = self.label.view { + if labelView.superview == nil { + labelView.isUserInteractionEnabled = false + self.backgroundView.contentView.addSubview(labelView) + } + labelView.frame = labelFrame + } + + let size = CGSize(width: 21.0 + labelSize.width + 18.0, height: 30.0) + self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: true, tintColor: .init(kind: .panel), isInteractive: true, transition: .immediate) + self.backgroundView.frame = CGRect(origin: .zero, size: size) + self.button.frame = CGRect(origin: .zero, size: size).insetBy(dx: -16.0, dy: -16.0) + + return size + } +} diff --git a/submodules/MediaPickerUI/Sources/LivePhotoButton.swift b/submodules/MediaPickerUI/Sources/LivePhotoButton.swift index 6c6c552222..a6ac357d3d 100644 --- a/submodules/MediaPickerUI/Sources/LivePhotoButton.swift +++ b/submodules/MediaPickerUI/Sources/LivePhotoButton.swift @@ -217,7 +217,7 @@ final class LivePhotoButton: UIView, TGLivePhotoButton { let size = CGSize(width: 19.0 + labelSize.width + 16.0, height: 18.0) self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: true, tintColor: .init(kind: .panel), isInteractive: true, transition: .immediate) self.backgroundView.frame = CGRect(origin: .zero, size: size) - self.button.frame = CGRect(origin: .zero, size: size) + self.button.frame = CGRect(origin: .zero, size: size).insetBy(dx: -16.0, dy: -16.0) } } diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/LivePhotoPlay.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Gallery/LivePhotoPlay.imageset/Contents.json new file mode 100644 index 0000000000..b6dcf45856 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Gallery/LivePhotoPlay.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "play1.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Gallery/LivePhotoPlay.imageset/play1.pdf b/submodules/TelegramUI/Images.xcassets/Media Gallery/LivePhotoPlay.imageset/play1.pdf new file mode 100644 index 0000000000000000000000000000000000000000..cb363de8949db5656fbd89eeee221400a73552dd GIT binary patch literal 9018 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 HSN(qgq#8%Y literal 0 HcmV?d00001 From b3d4c97da45b8932dbbfdde498138bf6fb152f27 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 30 Mar 2026 05:09:10 +0200 Subject: [PATCH 8/8] Various improvements --- .../Sources/GalleryControllerNode.swift | 2 +- .../Items/UniversalVideoGalleryItem.swift | 104 ++++++++------- .../Sources/MediaPickerScreen.swift | 118 ------------------ 3 files changed, 60 insertions(+), 164 deletions(-) diff --git a/submodules/GalleryUI/Sources/GalleryControllerNode.swift b/submodules/GalleryUI/Sources/GalleryControllerNode.swift index f0f6bbf31d..094fadb5fc 100644 --- a/submodules/GalleryUI/Sources/GalleryControllerNode.swift +++ b/submodules/GalleryUI/Sources/GalleryControllerNode.swift @@ -537,12 +537,12 @@ open class GalleryControllerNode: ASDisplayNode, ASScrollViewDelegate, ASGesture self.statusBar?.statusBarStyle = .White } self.navigationBar?.alpha = transition - self.footerNode.alpha = transition if let currentThumbnailContainerNode = self.currentThumbnailContainerNode, let layout = self.containerLayout?.1, layout.size.width < layout.size.height { currentThumbnailContainerNode.alpha = transition } } + self.footerNode.alpha = transition self.updateDismissTransition(transition) self.updateDistanceFromEquilibrium(distanceFromEquilibrium) diff --git a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift index 3077bd4ee4..007a2f5b50 100644 --- a/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift +++ b/submodules/GalleryUI/Sources/Items/UniversalVideoGalleryItem.swift @@ -222,6 +222,8 @@ private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentN private var context: AccountContext? private var adView = ComponentView() + private var livePhotoButton: LivePhotoButton? + private var livePhotoButtonIsPlaying = false private var message: Message? private var adContext: AdMessagesHistoryContext? @@ -234,7 +236,7 @@ private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentN var presentPremiumDemo: (() -> Void)? var openMoreMenu: ((UIView, Message) -> Void)? - private var validLayout: (size: CGSize, metrics: LayoutMetrics, insets: UIEdgeInsets)? + private var validLayout: (size: CGSize, metrics: LayoutMetrics, insets: UIEdgeInsets, isHidden: Bool)? deinit { self.adDisposable.dispose() @@ -281,34 +283,61 @@ private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentN } if let validLayout = self.validLayout { - self.updateLayout(size: validLayout.size, metrics: validLayout.metrics, insets: validLayout.insets, isHidden: false, transition: .immediate) + self.updateLayout(size: validLayout.size, metrics: validLayout.metrics, insets: validLayout.insets, isHidden: validLayout.isHidden, transition: .immediate) } })) } + func setLivePhotoButton(context: AccountContext, isVisible: Bool, isPlaying: Bool, pressed: @escaping () -> Void) { + self.livePhotoButtonIsPlaying = isPlaying + + if isVisible { + let livePhotoButton: LivePhotoButton + if let current = self.livePhotoButton { + livePhotoButton = current + } else { + livePhotoButton = LivePhotoButton(context: context) + self.livePhotoButton = livePhotoButton + } + livePhotoButton.pressed = pressed + + if let validLayout = self.validLayout { + self.updateLayout(size: validLayout.size, metrics: validLayout.metrics, insets: validLayout.insets, isHidden: validLayout.isHidden, transition: .immediate) + } + } else if let livePhotoButton = self.livePhotoButton { + self.livePhotoButton = nil + livePhotoButton.removeFromSuperview() + } + } + var timer: SwiftSignalKit.Timer? var hiddenMessages = Set() var isAnimatingOut = false var reportedMessages = Set() override func updateLayout(size: CGSize, metrics: LayoutMetrics, insets: UIEdgeInsets, isHidden: Bool, transition: ContainedViewLayoutTransition) { - self.validLayout = (size, metrics, insets) + self.validLayout = (size, metrics, insets, isHidden) - if self.timer == nil { + if self.timer == nil && self.adState != nil { self.timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] progress in guard let self else { return } if let validLayout = self.validLayout { - self.updateLayout(size: validLayout.size, metrics: validLayout.metrics, insets: validLayout.insets, isHidden: false, transition: .immediate) + self.updateLayout(size: validLayout.size, metrics: validLayout.metrics, insets: validLayout.insets, isHidden: validLayout.isHidden, transition: .immediate) } }, queue: Queue.mainQueue()) self.timer?.start() } - let isLandscape = size.width > size.height - let _ = isLandscape - + if let livePhotoButton = self.livePhotoButton { + let livePhotoButtonSize = livePhotoButton.update(isPlaying: self.livePhotoButtonIsPlaying) + if livePhotoButton.superview == nil { + self.view.addSubview(livePhotoButton) + } + transition.updateFrame(view: livePhotoButton, frame: CGRect(origin: CGPoint(x: 16.0, y: insets.top + 10.0), size: livePhotoButtonSize)) + } + let currentTime = Int32(CFAbsoluteTimeGetCurrent()) var currentAd: (Int32, Message?)? @@ -350,7 +379,7 @@ private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentN if available { self.hiddenMessages.insert(adMessage.id) if let validLayout = self.validLayout { - self.updateLayout(size: validLayout.size, metrics: validLayout.metrics, insets: validLayout.insets, isHidden: false, transition: .immediate) + self.updateLayout(size: validLayout.size, metrics: validLayout.metrics, insets: validLayout.insets, isHidden: validLayout.isHidden, transition: .immediate) } } else { self.presentPremiumDemo?() @@ -360,7 +389,7 @@ private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentN if let self, let ad = adMessage.adAttribute { self.hiddenMessages.insert(adMessage.id) if let validLayout = self.validLayout { - self.updateLayout(size: validLayout.size, metrics: validLayout.metrics, insets: validLayout.insets, isHidden: false, transition: .immediate) + self.updateLayout(size: validLayout.size, metrics: validLayout.metrics, insets: validLayout.insets, isHidden: validLayout.isHidden, transition: .immediate) } context.engine.messages.markAdAction(opaqueId: ad.opaqueId, media: false, fullscreen: false) self.performAction?(.url(url: ad.url, concealed: false, forceExternal: true, dismiss: false)) @@ -399,7 +428,9 @@ private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentN } override func animateIn(previousContentNode: GalleryOverlayContentNode?, transition: ContainedViewLayoutTransition) { - + if let livePhotoButton = self.livePhotoButton { + livePhotoButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } } override func animateOut(nextContentNode: GalleryOverlayContentNode?, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { @@ -407,6 +438,12 @@ private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentN } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let livePhotoButton = self.livePhotoButton { + let buttonPoint = self.view.convert(point, to: livePhotoButton) + if let result = livePhotoButton.hitTest(buttonPoint, with: event) { + return result + } + } if let adView = self.adView.view, adView.frame.contains(point) { return super.hitTest(point, with: event) } @@ -894,7 +931,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { private let statusButtonNode: HighlightableButtonNode private let statusNode: RadialStatusNode private var statusNodeShouldBeHidden = true - private var livePhotoButton: LivePhotoButton? private let playbackControls = ComponentView() @@ -1279,14 +1315,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { transition.setFrame(view: playbackControlsView, frame: playbackControlsFrame) } - if let livePhotoButton = self.livePhotoButton { - let livePhotoButtonSize = livePhotoButton.update(isPlaying: self.isLivePhotoPlaybackActive) - if livePhotoButton.superview == nil { - self.view.addSubview(livePhotoButton) - } - transition.updateFrame(view: livePhotoButton, frame: CGRect(origin: CGPoint(x: 16.0, y: max(navigationBarHeight, layout.statusBarHeight ?? 0.0) + 10.0), size: livePhotoButtonSize)) - } - if let pictureInPictureNode = self.pictureInPictureNode { if let item = self.item { var placeholderSize = item.content.dimensions.fitted(layout.size) @@ -2070,31 +2098,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } : nil))) } - if self.isLivePhoto { - let livePhotoButton: LivePhotoButton - if let current = self.livePhotoButton { - livePhotoButton = current - } else { - livePhotoButton = LivePhotoButton(context: self.context) - livePhotoButton.pressed = { [weak self] in - guard let self else { - return - } - self.updateLivePhotoPlayback(isActive: !self.isLivePhotoPlaybackActive, animated: true) - } - self.livePhotoButton = livePhotoButton - self.view.addSubview(livePhotoButton) - } -// if livePhotoButton.superview == nil { -// -// } -// if let validLayout = self.validLayout { -// self.containerLayoutUpdated(validLayout.layout, navigationBarHeight: validLayout.navigationBarHeight, transition: .immediate) -// } - } else if let livePhotoButton = self.livePhotoButton { - self.livePhotoButton = nil - livePhotoButton.removeFromSuperview() - } + self.updateLivePhotoButton() } override func controlsVisibilityUpdated(isVisible: Bool, animated: Bool) { @@ -2194,6 +2198,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { } self.isLivePhotoPlaybackActive = isActive + self.updateLivePhotoButton() if isActive { self.setLivePhotoVideoVisible(true, animated: animated) videoNode.seek(0.0) @@ -2213,6 +2218,15 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode { self.updateLivePhotoPlayback(isActive: false, animated: false) } + private func updateLivePhotoButton() { + self.overlayContentNode.setLivePhotoButton(context: self.context, isVisible: self.isLivePhoto, isPlaying: self.isLivePhotoPlaybackActive, pressed: { [weak self] in + guard let self else { + return + } + self.updateLivePhotoPlayback(isActive: !self.isLivePhotoPlaybackActive, animated: true) + }) + } + override func centralityUpdated(isCentral: Bool) { super.centralityUpdated(isCentral: isCentral) diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index f60c002367..6de88cee9e 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -2787,124 +2787,6 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att } } - -// if let cancelButton = self.cancelButton { -// if cancelButton.view == nil { -// buttonTransition = .immediate -// } -// let cancelButtonSize = cancelButton.update( -// transition: buttonTransition, -// component: AnyComponent(GlassBarButtonComponent( -// size: barButtonSize, -// backgroundColor: nil, -// isDark: self.presentationData.theme.overallDarkAppearance, -// state: .glass, -// component: AnyComponentWithIdentity(id: isBack ? "back" : "close", component: AnyComponent( -// BundleIconComponent( -// name: isBack ? "Navigation/Back" : "Navigation/Close", -// tintColor: self.presentationData.theme.chat.inputPanel.panelControlColor -// ) -// )), -// action: { [weak self] _ in -// self?.cancelPressed() -// } -// )), -// environment: {}, -// containerSize: barButtonSize -// ) -// let cancelButtonFrame = CGRect(origin: CGPoint(x: barButtonSideInset + layout.safeInsets.left, y: barButtonSideInset), size: cancelButtonSize) -// if let view = cancelButton.view { -// if view.superview == nil { -// self.view.addSubview(view) -// } -// view.bounds = CGRect(origin: .zero, size: cancelButtonFrame.size) -// view.center = cancelButtonFrame.center -// } -// } -// -// if moreIsVisible, case .glass = self.style { -// let moreButton: ComponentView -// if let current = self.rightButton { -// moreButton = current -// } else { -// moreButton = ComponentView() -// self.rightButton = moreButton -// } -// -// let buttonComponent: AnyComponent -// let rightButtonSize: CGSize -// if self.hasSelectButton { -// buttonComponent = AnyComponent(GlassBarButtonComponent( -// size: nil, -// backgroundColor: nil, -// isDark: self.presentationData.theme.overallDarkAppearance, -// state: .glass, -// component: AnyComponentWithIdentity(id: "select", component: AnyComponent( -// Text(text: self.presentationData.strings.Common_Select, font: Font.semibold(17.0), color: self.presentationData.theme.chat.inputPanel.panelControlColor) -// )), -// action: { [weak self] _ in -// self?.selectPressed() -// }) -// ) -// rightButtonSize = CGSize(width: 160.0, height: 44.0) -// } else { -// buttonComponent = AnyComponent(GlassBarButtonComponent( -// size: barButtonSize, -// backgroundColor: nil, -// isDark: self.presentationData.theme.overallDarkAppearance, -// state: .glass, -// component: AnyComponentWithIdentity(id: "more", component: AnyComponent( -// LottieComponent( -// content: LottieComponent.AppBundleContent( -// name: "anim_morewide" -// ), -// color: self.presentationData.theme.chat.inputPanel.panelControlColor, -// size: CGSize(width: 34.0, height: 34.0), -// playOnce: self.moreButtonPlayOnce -// ) -// )), -// action: { [weak self] view in -// self?.searchOrMorePressed(view: view, gesture: nil) -// self?.moreButtonPlayOnce.invoke(Void()) -// }) -// ) -// rightButtonSize = barButtonSize -// } -// -// let moreButtonSize = moreButton.update( -// transition: buttonTransition, -// component: buttonComponent, -// environment: {}, -// containerSize: rightButtonSize -// ) -// let moreButtonFrame = CGRect(origin: CGPoint(x: layout.size.width - moreButtonSize.width - barButtonSideInset - layout.safeInsets.right, y: barButtonSideInset), size: moreButtonSize) -// if let view = moreButton.view { -// if view.superview == nil { -// self.view.addSubview(view) -// if transition.isAnimated { -// let transition = ComponentTransition(transition) -// transition.animateAlpha(view: view, from: 0.0, to: 1.0) -// transition.animateScale(view: view, from: 0.1, to: 1.0) -// } -// } -// view.bounds = CGRect(origin: .zero, size: moreButtonFrame.size) -// view.center = moreButtonFrame.center -// } -// } else if let moreButton = self.rightButton { -// self.rightButton = nil -// if let view = moreButton.view { -// if transition.isAnimated { -// let transition = ComponentTransition(transition) -// view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in -// view.removeFromSuperview() -// }) -// transition.animateScale(view: view, from: 1.0, to: 0.1) -// } else { -// view.removeFromSuperview() -// } -// } -// } - if case .assets(_, .story) = self.subject { if self.selectionCount > 0 { let text = self.presentationData.strings.MediaPicker_CreateStory(self.selectionCount)