[WIP] Release changes

This commit is contained in:
Isaac 2024-01-26 15:33:01 +01:00
parent ce83d7510f
commit 953e1598f7
93 changed files with 4096 additions and 556 deletions

View File

@ -161,12 +161,16 @@ public struct ChatAvailableMessageActions {
public var banAuthor: Peer?
public var disableDelete: Bool
public var isCopyProtected: Bool
public var setTag: Bool
public var editTags: Set<MessageReaction.Reaction>
public init(options: ChatAvailableMessageActionOptions, banAuthor: Peer?, disableDelete: Bool, isCopyProtected: Bool) {
public init(options: ChatAvailableMessageActionOptions, banAuthor: Peer?, disableDelete: Bool, isCopyProtected: Bool, setTag: Bool, editTags: Set<MessageReaction.Reaction>) {
self.options = options
self.banAuthor = banAuthor
self.disableDelete = disableDelete
self.isCopyProtected = isCopyProtected
self.setTag = setTag
self.editTags = editTags
}
}
@ -931,7 +935,7 @@ public protocol SharedAccountContext: AnyObject {
func openStorageUsage(context: AccountContext)
func openLocationScreen(context: AccountContext, messageId: MessageId, navigationController: NavigationController)
func openExternalUrl(context: AccountContext, urlContext: OpenURLContext, url: String, forceExternal: Bool, presentationData: PresentationData, navigationController: NavigationController?, dismissInput: @escaping () -> Void)
func chatAvailableMessageActions(engine: TelegramEngine, accountPeerId: EnginePeer.Id, messageIds: Set<EngineMessage.Id>) -> Signal<ChatAvailableMessageActions, NoError>
func chatAvailableMessageActions(engine: TelegramEngine, accountPeerId: EnginePeer.Id, messageIds: Set<EngineMessage.Id>, keepUpdated: Bool) -> Signal<ChatAvailableMessageActions, NoError>
func chatAvailableMessageActions(engine: TelegramEngine, accountPeerId: EnginePeer.Id, messageIds: Set<EngineMessage.Id>, messages: [EngineMessage.Id: EngineMessage], peers: [EnginePeer.Id: EnginePeer]) -> Signal<ChatAvailableMessageActions, NoError>
func resolveUrl(context: AccountContext, peerId: PeerId?, url: String, skipUrlAuth: Bool) -> Signal<ResolvedUrl, NoError>
func resolveUrlWithProgress(context: AccountContext, peerId: PeerId?, url: String, skipUrlAuth: Bool) -> Signal<ResolveUrlResult, NoError>

View File

@ -56,6 +56,7 @@ public final class ChatMessageItemAssociatedData: Equatable {
public let chatThemes: [TelegramTheme]
public let deviceContactsNumbers: Set<String>
public let isStandalone: Bool
public let isInline: Bool
public init(
automaticDownloadPeerType: MediaAutoDownloadPeerType,
@ -85,7 +86,8 @@ public final class ChatMessageItemAssociatedData: Equatable {
audioTranscriptionTrial: AudioTranscription.TrialState = .defaultValue,
chatThemes: [TelegramTheme] = [],
deviceContactsNumbers: Set<String> = Set(),
isStandalone: Bool = false
isStandalone: Bool = false,
isInline: Bool = false
) {
self.automaticDownloadPeerType = automaticDownloadPeerType
self.automaticDownloadPeerId = automaticDownloadPeerId
@ -115,6 +117,7 @@ public final class ChatMessageItemAssociatedData: Equatable {
self.chatThemes = chatThemes
self.deviceContactsNumbers = deviceContactsNumbers
self.isStandalone = isStandalone
self.isInline = isInline
}
public static func == (lhs: ChatMessageItemAssociatedData, rhs: ChatMessageItemAssociatedData) -> Bool {
@ -199,6 +202,9 @@ public final class ChatMessageItemAssociatedData: Equatable {
if lhs.isStandalone != rhs.isStandalone {
return false
}
if lhs.isInline != rhs.isInline {
return false
}
return true
}
}

View File

@ -203,6 +203,8 @@ public final class DefaultAnimatedStickerNodeImpl: ASDisplayNode, AnimatedSticke
public private(set) var currentFrameCount: Int = 0
private var playFromIndex: Int?
public var frameColorUpdated: ((UIColor) -> Void)?
private let timer = Atomic<SwiftSignalKit.Timer?>(value: nil)
private let frameSource = Atomic<QueueLocalObject<AnimatedStickerFrameSourceWrapper>?>(value: nil)
@ -525,6 +527,11 @@ public final class DefaultAnimatedStickerNodeImpl: ASDisplayNode, AnimatedSticke
strongSelf.reportedStarted = true
strongSelf.started()
}
}, averageColor: strongSelf.frameColorUpdated == nil ? nil : { color in
guard let strongSelf = self else {
return
}
strongSelf.frameColorUpdated?(color)
})
strongSelf.frameUpdated(frame.index, frame.totalFrames)
@ -635,6 +642,11 @@ public final class DefaultAnimatedStickerNodeImpl: ASDisplayNode, AnimatedSticke
strongSelf.reportedStarted = true
strongSelf.started()
}
}, averageColor: strongSelf.frameColorUpdated == nil ? nil : { color in
guard let strongSelf = self else {
return
}
strongSelf.frameColorUpdated?(color)
})
strongSelf.frameUpdated(frame.index, frame.totalFrames)
@ -790,6 +802,11 @@ public final class DefaultAnimatedStickerNodeImpl: ASDisplayNode, AnimatedSticke
strongSelf.reportedStarted = true
strongSelf.started()
}
}, averageColor: strongSelf.frameColorUpdated == nil ? nil : { color in
guard let strongSelf = self else {
return
}
strongSelf.frameColorUpdated?(color)
})
strongSelf.playbackStatus.set(.single(AnimatedStickerStatus(playing: false, duration: duration, timestamp: 0.0)))

View File

@ -51,7 +51,7 @@ final class AnimationRendererPool {
protocol AnimationRenderer: ASDisplayNode {
var currentFrameImage: UIImage? { get }
func render(queue: Queue, width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, mulAlpha: Bool, completion: @escaping () -> Void)
func render(queue: Queue, width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, mulAlpha: Bool, completion: @escaping () -> Void, averageColor: ((UIColor) -> Void)?)
func setOverlayColor(_ color: UIColor?, replace: Bool, animated: Bool)
}

View File

@ -58,7 +58,7 @@ final class CompressedAnimationRenderer: ASDisplayNode, AnimationRenderer {
self.layer.backgroundColor = nil
}
func render(queue: Queue, width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, mulAlpha: Bool, completion: @escaping () -> Void) {
func render(queue: Queue, width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, mulAlpha: Bool, completion: @escaping () -> Void, averageColor: ((UIColor) -> Void)?) {
switch type {
case .dct:
self.renderer.renderIdct(layer: self.layer as! MetalImageLayer, compressedImage: AnimationCompressor.CompressedImageData(data: data), completion: { [weak self] in

View File

@ -28,7 +28,7 @@ final class SoftwareAnimationRenderer: ASDisplayNode, AnimationRenderer {
}
}
func render(queue: Queue, width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, mulAlpha: Bool, completion: @escaping () -> Void) {
func render(queue: Queue, width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, mulAlpha: Bool, completion: @escaping () -> Void, averageColor: ((UIColor) -> Void)?) {
assert(bytesPerRow > 0)
let renderAsTemplateImage = self.renderAsTemplateImage
queue.async { [weak self] in
@ -43,6 +43,7 @@ final class SoftwareAnimationRenderer: ASDisplayNode, AnimationRenderer {
}
var image: UIImage?
var averageColorValue: UIColor?
autoreleasepool {
image = generateImagePixel(CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, pixelGenerator: { _, pixelData, contextBytesPerRow in
@ -83,6 +84,99 @@ final class SoftwareAnimationRenderer: ASDisplayNode, AnimationRenderer {
if renderAsTemplateImage {
image = image?.withRenderingMode(.alwaysTemplate)
}
if averageColor != nil {
let blurredWidth = 16
let blurredHeight = 16
let blurredBytesPerRow = blurredWidth * 4
guard let context = DrawingContext(size: CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)), scale: 1.0, opaque: true, bytesPerRow: blurredBytesPerRow) else {
return
}
let size = CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight))
if let image, let cgImage = image.cgImage {
context.withFlippedContext { c in
c.setFillColor(UIColor.white.cgColor)
c.fill(CGRect(origin: CGPoint(), size: size))
c.draw(cgImage, in: CGRect(origin: CGPoint(x: -size.width / 2.0, y: -size.height / 2.0), size: CGSize(width: size.width * 1.8, height: size.height * 1.8)))
}
}
var destinationBuffer = vImage_Buffer()
destinationBuffer.width = UInt(blurredWidth)
destinationBuffer.height = UInt(blurredHeight)
destinationBuffer.data = context.bytes
destinationBuffer.rowBytes = context.bytesPerRow
vImageBoxConvolve_ARGB8888(&destinationBuffer,
&destinationBuffer,
nil,
0, 0,
UInt32(15),
UInt32(15),
nil,
vImage_Flags(kvImageTruncateKernel))
let divisor: Int32 = 0x1000
let rwgt: CGFloat = 0.3086
let gwgt: CGFloat = 0.6094
let bwgt: CGFloat = 0.0820
let adjustSaturation: CGFloat = 1.7
let a = (1.0 - adjustSaturation) * rwgt + adjustSaturation
let b = (1.0 - adjustSaturation) * rwgt
let c = (1.0 - adjustSaturation) * rwgt
let d = (1.0 - adjustSaturation) * gwgt
let e = (1.0 - adjustSaturation) * gwgt + adjustSaturation
let f = (1.0 - adjustSaturation) * gwgt
let g = (1.0 - adjustSaturation) * bwgt
let h = (1.0 - adjustSaturation) * bwgt
let i = (1.0 - adjustSaturation) * bwgt + adjustSaturation
let satMatrix: [CGFloat] = [
a, b, c, 0,
d, e, f, 0,
g, h, i, 0,
0, 0, 0, 1
]
var matrix: [Int16] = satMatrix.map { value in
return Int16(value * CGFloat(divisor))
}
vImageMatrixMultiply_ARGB8888(&destinationBuffer, &destinationBuffer, &matrix, divisor, nil, nil, vImage_Flags(kvImageDoNotTile))
context.withFlippedContext { c in
c.setFillColor(UIColor.white.withMultipliedAlpha(0.1).cgColor)
c.fill(CGRect(origin: CGPoint(), size: size))
}
var sumR: UInt64 = 0
var sumG: UInt64 = 0
var sumB: UInt64 = 0
var sumA: UInt64 = 0
for y in 0 ..< blurredHeight {
let row = context.bytes.assumingMemoryBound(to: UInt8.self).advanced(by: y * blurredBytesPerRow)
for x in 0 ..< blurredWidth {
let pixel = row.advanced(by: x * 4)
sumB += UInt64(pixel.advanced(by: 0).pointee)
sumG += UInt64(pixel.advanced(by: 1).pointee)
sumR += UInt64(pixel.advanced(by: 2).pointee)
sumA += UInt64(pixel.advanced(by: 3).pointee)
}
}
sumR /= UInt64(blurredWidth * blurredHeight)
sumG /= UInt64(blurredWidth * blurredHeight)
sumB /= UInt64(blurredWidth * blurredHeight)
sumA /= UInt64(blurredWidth * blurredHeight)
sumA = 255
averageColorValue = UIColor(red: CGFloat(sumR) / 255.0, green: CGFloat(sumG) / 255.0, blue: CGFloat(sumB) / 255.0, alpha: CGFloat(sumA) / 255.0)
}
}
Queue.mainQueue().async {
@ -100,6 +194,10 @@ final class SoftwareAnimationRenderer: ASDisplayNode, AnimationRenderer {
strongSelf.highlightedContentNode?.frame = strongSelf.bounds
}
completion()
if let averageColor, let averageColorValue {
averageColor(averageColorValue)
}
}
}
}

View File

@ -1866,8 +1866,24 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
|> distinctUntilChanged
let preferHighQualityStories: Signal<Bool, NoError> = combineLatest(
context.sharedContext.automaticMediaDownloadSettings
|> map { settings in
return settings.highQualityStories
}
|> distinctUntilChanged,
context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)
)
)
|> map { setting, peer -> Bool in
let isPremium = peer?.isPremium ?? false
return setting && isPremium
}
|> distinctUntilChanged
self.preloadStorySubscriptionsDisposable = (combineLatest(queue: .mainQueue(),
self.context.engine.messages.preloadStorySubscriptions(isHidden: self.location == .chatList(groupId: .archive)),
self.context.engine.messages.preloadStorySubscriptions(isHidden: self.location == .chatList(groupId: .archive), preferHighQuality: preferHighQualityStories),
self.context.sharedContext.automaticMediaDownloadSettings,
automaticDownloadNetworkType
)

View File

@ -267,6 +267,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView {
var foreground: UInt32
var extractedBackground: UInt32
var extractedForeground: UInt32
var extractedSelectedForeground: UInt32
var isSelected: Bool
}
@ -379,9 +380,13 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView {
func drawContents(colors: Colors) {
let backgroundColor: UIColor
let foregroundColor: UIColor
if isExtracted && !layout.colors.isSelected {
if isExtracted {
backgroundColor = UIColor(argb: colors.extractedBackground)
foregroundColor = UIColor(argb: colors.extractedForeground)
if layout.colors.isSelected {
foregroundColor = UIColor(argb: colors.extractedSelectedForeground)
} else {
foregroundColor = UIColor(argb: colors.extractedForeground)
}
} else {
backgroundColor = UIColor(argb: colors.background)
foregroundColor = UIColor(argb: colors.foreground)
@ -702,6 +707,7 @@ public final class ReactionButtonAsyncNode: ContextControllerSourceView {
foreground: spec.component.chosenOrder != nil ? spec.component.colors.selectedForeground : spec.component.colors.deselectedForeground,
extractedBackground: spec.component.colors.extractedBackground,
extractedForeground: spec.component.colors.extractedForeground,
extractedSelectedForeground: spec.component.colors.extractedSelectedForeground,
isSelected: spec.component.chosenOrder != nil
)
var backgroundCounter: ReactionButtonAsyncNode.ContainerButtonNode.Counter?
@ -1030,6 +1036,7 @@ public final class ReactionButtonComponent: Equatable {
public var selectedForeground: UInt32
public var extractedBackground: UInt32
public var extractedForeground: UInt32
public var extractedSelectedForeground: UInt32
public var deselectedMediaPlaceholder: UInt32
public var selectedMediaPlaceholder: UInt32
@ -1040,6 +1047,7 @@ public final class ReactionButtonComponent: Equatable {
selectedForeground: UInt32,
extractedBackground: UInt32,
extractedForeground: UInt32,
extractedSelectedForeground: UInt32,
deselectedMediaPlaceholder: UInt32,
selectedMediaPlaceholder: UInt32
) {
@ -1049,6 +1057,7 @@ public final class ReactionButtonComponent: Equatable {
self.selectedForeground = selectedForeground
self.extractedBackground = extractedBackground
self.extractedForeground = extractedForeground
self.extractedSelectedForeground = extractedSelectedForeground
self.deselectedMediaPlaceholder = deselectedMediaPlaceholder
self.selectedMediaPlaceholder = selectedMediaPlaceholder
}

View File

@ -2255,6 +2255,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
public var reactionItems: [ReactionContextItem]
public var selectedReactionItems: Set<MessageReaction.Reaction>
public var reactionsTitle: String?
public var reactionsLocked: Bool
public var animationCache: AnimationCache?
public var alwaysAllowPremiumReactions: Bool
public var allPresetReactionsAreAvailable: Bool
@ -2271,6 +2272,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
reactionItems: [ReactionContextItem] = [],
selectedReactionItems: Set<MessageReaction.Reaction> = Set(),
reactionsTitle: String? = nil,
reactionsLocked: Bool = false,
animationCache: AnimationCache? = nil,
alwaysAllowPremiumReactions: Bool = false,
allPresetReactionsAreAvailable: Bool = false,
@ -2287,6 +2289,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
self.reactionItems = reactionItems
self.selectedReactionItems = selectedReactionItems
self.reactionsTitle = reactionsTitle
self.reactionsLocked = reactionsLocked
self.alwaysAllowPremiumReactions = alwaysAllowPremiumReactions
self.allPresetReactionsAreAvailable = allPresetReactionsAreAvailable
self.getEmojiContent = getEmojiContent
@ -2303,6 +2306,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
self.reactionItems = []
self.selectedReactionItems = Set()
self.reactionsTitle = nil
self.reactionsLocked = false
self.alwaysAllowPremiumReactions = false
self.allPresetReactionsAreAvailable = false
self.getEmojiContent = nil

View File

@ -40,16 +40,18 @@ public struct ContextControllerReactionItems {
public var reactionItems: [ReactionContextItem]
public var selectedReactionItems: Set<MessageReaction.Reaction>
public var reactionsTitle: String?
public var reactionsLocked: Bool
public var animationCache: AnimationCache
public var alwaysAllowPremiumReactions: Bool
public var allPresetReactionsAreAvailable: Bool
public var getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?
public init(context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, reactionsTitle: String?, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, allPresetReactionsAreAvailable: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?) {
public init(context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, reactionsTitle: String?, reactionsLocked: Bool, animationCache: AnimationCache, alwaysAllowPremiumReactions: Bool, allPresetReactionsAreAvailable: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?) {
self.context = context
self.reactionItems = reactionItems
self.selectedReactionItems = selectedReactionItems
self.reactionsTitle = reactionsTitle
self.reactionsLocked = reactionsLocked
self.animationCache = animationCache
self.alwaysAllowPremiumReactions = alwaysAllowPremiumReactions
self.allPresetReactionsAreAvailable = allPresetReactionsAreAvailable
@ -1075,6 +1077,7 @@ func makeContextControllerActionsStackItem(items: ContextController.Items) -> [C
reactionItems: items.reactionItems,
selectedReactionItems: items.selectedReactionItems,
reactionsTitle: items.reactionsTitle,
reactionsLocked: items.reactionsLocked,
animationCache: animationCache,
alwaysAllowPremiumReactions: items.alwaysAllowPremiumReactions,
allPresetReactionsAreAvailable: items.allPresetReactionsAreAvailable,

View File

@ -649,6 +649,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
items: reactionItems.reactionItems,
selectedItems: reactionItems.selectedReactionItems,
title: reactionItems.reactionsTitle,
reactionsLocked: reactionItems.reactionsLocked,
alwaysAllowPremiumReactions: reactionItems.alwaysAllowPremiumReactions,
allPresetReactionsAreAvailable: reactionItems.allPresetReactionsAreAvailable,
getEmojiContent: reactionItems.getEmojiContent,
@ -691,6 +692,13 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
return
}
if let reactionItems = strongSelf.actionsStackNode.topReactionItems, !reactionItems.reactionItems.isEmpty {
if reactionItems.allPresetReactionsAreAvailable {
controller.premiumReactionsSelected?()
return
}
}
if let file = file, let reactionContextNode = strongSelf.reactionContextNode {
let position: UndoOverlayController.Position
let insets = validLayout.insets(options: .statusBar)

View File

@ -72,8 +72,6 @@ private enum DebugControllerEntry: ItemListNodeEntry {
case keepChatNavigationStack(PresentationTheme, Bool)
case skipReadHistory(PresentationTheme, Bool)
case dustEffect(Bool)
case callV2(Bool)
case alternativeStoryMedia(Bool)
case crashOnSlowQueries(PresentationTheme, Bool)
case crashOnMemoryPressure(PresentationTheme, Bool)
case clearTips(PresentationTheme)
@ -126,7 +124,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return DebugControllerSection.logging.rawValue
case .webViewInspection, .resetWebViewCache:
return DebugControllerSection.web.rawValue
case .keepChatNavigationStack, .skipReadHistory, .dustEffect, .callV2, .alternativeStoryMedia, .crashOnSlowQueries, .crashOnMemoryPressure:
case .keepChatNavigationStack, .skipReadHistory, .dustEffect, .crashOnSlowQueries, .crashOnMemoryPressure:
return DebugControllerSection.experiments.rawValue
case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .storiesExperiment, .storiesJpegExperiment, .playlistPlayback, .enableQuickReactionSwitch, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .inlineForums, .localTranscription, .enableReactionOverrides, .restorePurchases:
return DebugControllerSection.experiments.rawValue
@ -179,10 +177,6 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return 16
case .dustEffect:
return 17
case .callV2:
return 18
case .alternativeStoryMedia:
return 19
case .crashOnSlowQueries:
return 20
case .crashOnMemoryPressure:
@ -954,22 +948,6 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return settings
}).start()
})
case let .callV2(value):
return ItemListSwitchItem(presentationData: presentationData, title: "CallV2", value: value, sectionId: self.section, style: .blocks, updated: { value in
let _ = updateExperimentalUISettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in
var settings = settings
settings.callV2 = value
return settings
}).start()
})
case let .alternativeStoryMedia(value):
return ItemListSwitchItem(presentationData: presentationData, title: "Story Data Saver", value: value, sectionId: self.section, style: .blocks, updated: { value in
let _ = updateExperimentalUISettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in
var settings = settings
settings.alternativeStoryMedia = value
return settings
}).start()
})
case let .crashOnSlowQueries(_, value):
return ItemListSwitchItem(presentationData: presentationData, title: "Crash when slow", value: value, sectionId: self.section, style: .blocks, updated: { value in
let _ = updateExperimentalUISettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in
@ -1436,8 +1414,6 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present
entries.append(.skipReadHistory(presentationData.theme, experimentalSettings.skipReadHistory))
#endif
entries.append(.dustEffect(experimentalSettings.dustEffect))
entries.append(.callV2(experimentalSettings.callV2))
entries.append(.alternativeStoryMedia(experimentalSettings.alternativeStoryMedia))
}
entries.append(.crashOnSlowQueries(presentationData.theme, experimentalSettings.crashOnLongQueries))
entries.append(.crashOnMemoryPressure(presentationData.theme, experimentalSettings.crashOnMemoryPressure))

View File

@ -136,6 +136,7 @@ public class DrawingReactionEntityView: DrawingStickerEntityView {
items: reactionItems.map(ReactionContextItem.reaction),
selectedItems: Set(),
title: nil,
reactionsLocked: false,
alwaysAllowPremiumReactions: false,
allPresetReactionsAreAvailable: false,
getEmojiContent: { [weak self] animationCache, animationRenderer in

View File

@ -1411,7 +1411,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
}
private func commitDeleteMessages(_ messages: [EngineMessage], ask: Bool) {
self.messageContextDisposable.set((self.context.sharedContext.chatAvailableMessageActions(engine: self.context.engine, accountPeerId: self.context.account.peerId, messageIds: Set(messages.map { $0.id })) |> deliverOnMainQueue).start(next: { [weak self] actions in
self.messageContextDisposable.set((self.context.sharedContext.chatAvailableMessageActions(engine: self.context.engine, accountPeerId: self.context.account.peerId, messageIds: Set(messages.map { $0.id }), keepUpdated: false) |> deliverOnMainQueue).start(next: { [weak self] actions in
if let strongSelf = self, let controllerInteration = strongSelf.controllerInteraction, !actions.options.isEmpty {
var presentationData = strongSelf.presentationData
if !presentationData.theme.overallDarkAppearance {

View File

@ -35,6 +35,8 @@ swift_library(
"//submodules/GZip:GZip",
"//submodules/ShimmerEffect:ShimmerEffect",
"//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage",
"//submodules/Components/BalancedTextComponent",
"//submodules/Markdown",
],
visibility = [
"//visibility:public",

View File

@ -21,6 +21,8 @@ import MultiAnimationRenderer
import EmojiTextAttachmentView
import TextFormat
import GZip
import BalancedTextComponent
import Markdown
public final class ReactionItem {
public struct Reaction: Equatable {
@ -124,6 +126,8 @@ private final class TitleLabelView: UIView {
let contentView = ComponentView<Empty>()
let tintContentView = ComponentView<Empty>()
var action: (() -> Void)?
override init(frame: CGRect) {
super.init(frame: frame)
}
@ -132,7 +136,7 @@ private final class TitleLabelView: UIView {
fatalError("init(coder:) has not been implemented")
}
func update(size: CGSize, text: String, theme: PresentationTheme, transition: ContainedViewLayoutTransition) {
func update(width: CGFloat, text: String, theme: PresentationTheme, transition: ContainedViewLayoutTransition) -> CGFloat {
let foregroundColor: UIColor
if theme.overallDarkAppearance {
foregroundColor = UIColor(white: 1.0, alpha: 0.5)
@ -140,17 +144,56 @@ private final class TitleLabelView: UIView {
foregroundColor = UIColor(white: 0.5, alpha: 0.9)
}
let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: foregroundColor)
let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: foregroundColor)
let link = MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.list.itemAccentColor, additionalAttributes: ["URL": true as NSNumber])
let attributes = MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in
return nil
})
let tintBody = MarkdownAttributeSet(font: Font.regular(13.0), textColor: .white)
let tintBold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: .white)
let tintLink = MarkdownAttributeSet(font: Font.regular(13.0), textColor: .white, additionalAttributes: [TelegramTextAttributes.URL: true as NSNumber])
let tintAttributes = MarkdownAttributes(body: tintBody, bold: tintBold, link: tintLink, linkAttribute: { _ in
return (TelegramTextAttributes.URL, "")
})
let contentSize = self.contentView.update(
transition: .immediate,
component: AnyComponent(Text(text: text, font: Font.regular(13.0), color: foregroundColor)),
component: AnyComponent(BalancedTextComponent(
text: .markdown(text: text, attributes: attributes),
balanced: true,
horizontalAlignment: .center,
maximumNumberOfLines: 0,
highlightColor: theme.list.itemAccentColor.withMultipliedAlpha(0.1),
highlightAction: { attributes in
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
return NSAttributedString.Key(rawValue: "URL")
} else {
return nil
}
}, tapAction: { [weak self] attributes, _ in
guard let self else {
return
}
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
self.action?()
}
}
)),
environment: {},
containerSize: size
containerSize: CGSize(width: width - 8.0 * 2.0, height: 10000.0)
)
let _ = self.tintContentView.update(
transition: .immediate,
component: AnyComponent(Text(text: text, font: Font.regular(13.0), color: .white)),
component: AnyComponent(BalancedTextComponent(
text: .markdown(text: text, attributes: tintAttributes),
balanced: true,
horizontalAlignment: .center,
maximumNumberOfLines: 0
)),
environment: {},
containerSize: size
containerSize: CGSize(width: width - 8.0 * 2.0, height: 10000.0)
)
if let contentView = self.contentView.view {
@ -158,8 +201,10 @@ private final class TitleLabelView: UIView {
contentView.layer.rasterizationScale = UIScreenScale
self.addSubview(contentView)
}
transition.updateFrame(view: contentView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - contentSize.width) / 2.0), y: 6.0), size: contentSize))
transition.updateFrame(view: contentView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((width - contentSize.width) / 2.0), y: 6.0), size: contentSize))
}
return 6.0 + contentSize.height
}
}
@ -252,7 +297,9 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
private let expandItemView: ExpandItemView?
private let title: String?
private let reactionsLocked: Bool
private var titleLabelView: TitleLabelView?
private var titleLabelHeight: CGFloat?
private var reactionSelectionComponentHost: ComponentView<Empty>?
@ -380,7 +427,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
}
}
public init(context: AccountContext, animationCache: AnimationCache, presentationData: PresentationData, items: [ReactionContextItem], selectedItems: Set<MessageReaction.Reaction>, title: String? = nil, alwaysAllowPremiumReactions: Bool, allPresetReactionsAreAvailable: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?, isExpandedUpdated: @escaping (ContainedViewLayoutTransition) -> Void, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, requestUpdateOverlayWantsToBeBelowKeyboard: @escaping (ContainedViewLayoutTransition) -> Void) {
public init(context: AccountContext, animationCache: AnimationCache, presentationData: PresentationData, items: [ReactionContextItem], selectedItems: Set<MessageReaction.Reaction>, title: String? = nil, reactionsLocked: Bool, alwaysAllowPremiumReactions: Bool, allPresetReactionsAreAvailable: Bool, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?, isExpandedUpdated: @escaping (ContainedViewLayoutTransition) -> Void, requestLayout: @escaping (ContainedViewLayoutTransition) -> Void, requestUpdateOverlayWantsToBeBelowKeyboard: @escaping (ContainedViewLayoutTransition) -> Void) {
self.context = context
self.presentationData = presentationData
self.items = items
@ -389,6 +436,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
self.isExpandedUpdated = isExpandedUpdated
self.requestLayout = requestLayout
self.requestUpdateOverlayWantsToBeBelowKeyboard = requestUpdateOverlayWantsToBeBelowKeyboard
self.reactionsLocked = reactionsLocked
self.animationCache = animationCache
self.animationRenderer = MultiAnimationRendererImpl()
@ -458,11 +506,11 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
context.setFillColor(shadowColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: gradientWidth - 1.0, dy: gradientWidth - 1.0))
})?.stretchableImage(withLeftCapWidth: Int(46.0 / 2.0), topCapHeight: Int(46.0 / 2.0))
if self.getEmojiContent == nil {
if self.getEmojiContent == nil || self.reactionsLocked {
self.contentContainer.view.mask = self.contentContainerMask
}
if getEmojiContent != nil {
if getEmojiContent != nil && !self.reactionsLocked {
let expandItemView = ExpandItemView()
self.expandItemView = expandItemView
@ -477,7 +525,6 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
let titleLabelView = TitleLabelView(frame: CGRect())
self.titleLabelView = titleLabelView
self.contentContainer.view.addSubview(titleLabelView)
self.contentTopInset = 24.0
}
self.alwaysAllowPremiumReactions = alwaysAllowPremiumReactions
@ -492,6 +539,15 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
self.addSubnode(self.contentContainer)
self.addSubnode(self.previewingItemContainer)
if let titleLabelView = self.titleLabelView {
titleLabelView.action = { [weak self] in
guard let self else {
return
}
self.premiumReactionsSelected?(nil)
}
}
self.availableReactionsDisposable = (context.engine.stickers.availableReactions()
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] availableReactions in
@ -513,7 +569,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
})
}
if let getEmojiContent = getEmojiContent {
if let getEmojiContent = getEmojiContent, !self.reactionsLocked {
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks)
self.stableEmptyResultEmojiDisposable.set((self.context.account.postbox.combinedView(keys: [viewKey])
|> take(1)
@ -823,7 +879,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
let effectiveItemSpacing: CGFloat = minItemSpacing + (1.0 - compressionFactor) * (itemSpacing - minItemSpacing)
var topVisibleItems: Int
if self.getEmojiContent != nil {
if !self.reactionsLocked && self.getEmojiContent != nil {
topVisibleItems = min(self.items.count, itemLayout.visibleItemCount)
} else {
topVisibleItems = self.items.count
@ -923,7 +979,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
itemTransition = .immediate
if case let .reaction(item) = self.items[i] {
itemNode = ReactionNode(context: self.context, theme: self.presentationData.theme, item: item, animationCache: self.animationCache, animationRenderer: self.animationRenderer, loopIdle: loopIdle)
itemNode = ReactionNode(context: self.context, theme: self.presentationData.theme, item: item, animationCache: self.animationCache, animationRenderer: self.animationRenderer, loopIdle: loopIdle, isLocked: self.reactionsLocked)
maskNode = nil
} else {
itemNode = PremiumReactionsNode(theme: self.presentationData.theme)
@ -967,7 +1023,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
}
}
if self.getEmojiContent != nil && i == itemLayout.visibleItemCount - 1 {
if !self.reactionsLocked && self.getEmojiContent != nil && i == itemLayout.visibleItemCount - 1 {
itemFrame.origin.x -= (1.0 - compressionFactor) * selectionItemFrame.width * 0.5
selectionItemFrame.origin.x -= (1.0 - compressionFactor) * selectionItemFrame.width * 0.5
itemNode.isUserInteractionEnabled = false
@ -1008,7 +1064,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
itemNode.appear(animated: !self.context.sharedContext.currentPresentationData.with({ $0 }).reduceMotion && !self.reduceMotion)
}
if self.getEmojiContent != nil, i == itemLayout.visibleItemCount - 1, let itemNode = itemNode as? ReactionNode {
if !self.reactionsLocked, self.getEmojiContent != nil, i == itemLayout.visibleItemCount - 1, let itemNode = itemNode as? ReactionNode {
let itemScale: CGFloat = 0.001 * (1.0 - compressionFactor) + normalItemScale * compressionFactor
transition.updateSublayerTransformScale(node: itemNode, scale: itemScale)
transition.updateTransformScale(layer: itemNode.selectionView.layer, scale: CGPoint(x: itemScale, y: itemScale))
@ -1029,22 +1085,6 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
}
}
if let title = self.title, let titleLabelView = self.titleLabelView {
let baseTitleFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.scrollNode.view.bounds.width, height: 20.0))
transition.updateFrame(view: titleLabelView, frame: baseTitleFrame)
titleLabelView.update(size: baseTitleFrame.size, text: title, theme: self.presentationData.theme, transition: transition)
transition.updateAlpha(layer: titleLabelView.layer, alpha: self.isExpanded ? 0.0 : 1.0)
if let titleView = titleLabelView.contentView.view, let tintContentView = titleLabelView.tintContentView.view {
if tintContentView.superview == nil {
tintContentView.layer.rasterizationScale = UIScreenScale
self.contentTintContainer.view.addSubview(tintContentView)
}
transition.updateFrame(view: tintContentView, frame: titleView.frame.offsetBy(dx: baseTitleFrame.minX, dy: baseTitleFrame.minY))
transition.updateAlpha(layer: tintContentView.layer, alpha: self.isExpanded ? 0.0 : 1.0)
}
}
if let expandItemView = self.expandItemView {
let expandItemSize: CGFloat
let expandTintOffset: CGFloat
@ -1114,7 +1154,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
var visibleContentWidth: CGFloat
var completeContentWidth: CGFloat
if self.getEmojiContent != nil {
if !self.reactionsLocked && self.getEmojiContent != nil {
let totalItemSlotCount = self.items.count + 1
var maxRowItemCount = Int(floor((size.width - sideInset * 2.0 - externalSideInset * 2.0 - itemSpacing) / (itemSize + itemSpacing)))
@ -1148,6 +1188,28 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
}
}
if let title = self.title, let titleLabelView = self.titleLabelView {
let titleLabelHeight = titleLabelView.update(width: visibleContentWidth, text: title, theme: self.presentationData.theme, transition: transition)
self.titleLabelHeight = titleLabelHeight
let baseTitleFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: visibleContentWidth, height: titleLabelHeight))
transition.updateFrame(view: titleLabelView, frame: baseTitleFrame)
transition.updateAlpha(layer: titleLabelView.layer, alpha: self.isExpanded ? 0.0 : 1.0)
if let titleView = titleLabelView.contentView.view, let tintContentView = titleLabelView.tintContentView.view {
if tintContentView.superview == nil {
tintContentView.layer.rasterizationScale = UIScreenScale
self.contentTintContainer.view.addSubview(tintContentView)
}
transition.updateFrame(view: tintContentView, frame: titleView.frame.offsetBy(dx: baseTitleFrame.minX, dy: baseTitleFrame.minY))
transition.updateAlpha(layer: tintContentView.layer, alpha: self.isExpanded ? 0.0 : 1.0)
}
if !self.isExpanded {
self.contentTopInset = titleLabelHeight
}
}
let contentHeight = verticalInset * 2.0 + rowHeight
var backgroundInsets = insets
@ -1196,7 +1258,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
itemSpacing: itemSpacing
)
if (self.isExpanded || self.reactionSelectionComponentHost != nil), let _ = self.getEmojiContent {
if (self.isExpanded || self.reactionSelectionComponentHost != nil), let _ = self.getEmojiContent, !self.reactionsLocked {
let reactionSelectionComponentHost: ComponentView<Empty>
var componentTransition = Transition(transition)
if let current = self.reactionSelectionComponentHost {
@ -1438,6 +1500,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
if case .locked = item.icon {
strongSelf.premiumReactionsSelected?(reactionItem.stillAnimation)
} else if strongSelf.reactionsLocked {
strongSelf.premiumReactionsSelected?(reactionItem.stillAnimation)
} else {
strongSelf.customReactionSource = (sourceView, sourceRect, sourceLayer, reactionItem)
strongSelf.reactionSelected?(updateReaction, isLongPress)
@ -1460,6 +1524,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
strongSelf.customReactionSource = (sourceView, sourceRect, sourceLayer, reactionItem)
if case .locked = item.icon {
strongSelf.premiumReactionsSelected?(reactionItem.stillAnimation)
} else if strongSelf.reactionsLocked {
strongSelf.premiumReactionsSelected?(reactionItem.stillAnimation)
} else {
strongSelf.reactionSelected?(reactionItem.updateMessageReaction, isLongPress)
}
@ -1851,7 +1917,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
guard let itemNode = self.visibleItemNodes[i] else {
continue
}
if let itemLayout = self.itemLayout, self.getEmojiContent != nil, i == itemLayout.visibleItemCount - 1 {
if let itemLayout = self.itemLayout, !self.reactionsLocked, self.getEmojiContent != nil, i == itemLayout.visibleItemCount - 1 {
itemNode.appear(animated: false)
continue
}
@ -2022,7 +2088,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
}
if let customReactionSource = self.customReactionSource {
let itemNode = ReactionNode(context: self.context, theme: self.presentationData.theme, item: customReactionSource.item, animationCache: self.animationCache, animationRenderer: self.animationRenderer, loopIdle: false, useDirectRendering: false)
let itemNode = ReactionNode(context: self.context, theme: self.presentationData.theme, item: customReactionSource.item, animationCache: self.animationCache, animationRenderer: self.animationRenderer, loopIdle: false, isLocked: false, useDirectRendering: false)
if let contents = customReactionSource.layer.contents {
itemNode.setCustomContents(contents: contents)
}
@ -2350,6 +2416,12 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.isExpanded, let titleLabelView = self.titleLabelView {
if let result = titleLabelView.hitTest(self.view.convert(point, to: titleLabelView), with: event) {
return result
}
}
let contentPoint = self.contentContainer.view.convert(point, from: self.view)
if self.contentContainer.bounds.contains(contentPoint) {
return self.contentContainer.hitTest(contentPoint, with: event)
@ -2446,6 +2518,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
case let .reaction(reactionItem):
if case .custom = reactionItem.updateMessageReaction, let hasPremium = self.hasPremium, !hasPremium, !self.allPresetReactionsAreAvailable {
self.premiumReactionsSelected?(reactionItem.stillAnimation)
} else if self.reactionsLocked {
self.premiumReactionsSelected?(reactionItem.stillAnimation)
} else {
self.reactionSelected?(reactionItem.updateMessageReaction, false)
}
@ -2603,6 +2677,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
if let itemNode = itemNode as? ReactionNode, itemNode.item.reaction == reaction {
if case .custom = itemNode.item.updateMessageReaction, let hasPremium = self.hasPremium, !hasPremium {
self.premiumReactionsSelected?(itemNode.item.stillAnimation)
} else if self.reactionsLocked {
self.premiumReactionsSelected?(itemNode.item.stillAnimation)
} else {
self.reactionSelected?(itemNode.item.updateMessageReaction, isLarge)
}
@ -2670,7 +2746,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode {
itemNode = currentItemNode
} else {
let animationRenderer = MultiAnimationRendererImpl()
itemNode = ReactionNode(context: context, theme: theme, item: reaction, animationCache: animationCache, animationRenderer: animationRenderer, loopIdle: false)
itemNode = ReactionNode(context: context, theme: theme, item: reaction, animationCache: animationCache, animationRenderer: animationRenderer, loopIdle: false, isLocked: false)
}
self.itemNode = itemNode

View File

@ -48,11 +48,15 @@ protocol ReactionItemNode: ASDisplayNode {
func updateLayout(size: CGSize, isExpanded: Bool, largeExpanded: Bool, isPreviewing: Bool, transition: ContainedViewLayoutTransition)
}
private let lockedBackgroundImage: UIImage = generateFilledCircleImage(diameter: 12.0, color: .white)!.withRenderingMode(.alwaysTemplate)
private let lockedBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeLock"), color: .white)
public final class ReactionNode: ASDisplayNode, ReactionItemNode {
let context: AccountContext
let theme: PresentationTheme
let item: ReactionItem
private let loopIdle: Bool
private let isLocked: Bool
private let hasAppearAnimation: Bool
private let useDirectRendering: Bool
@ -66,6 +70,9 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
private var customContentsNode: ASDisplayNode?
private var animationNode: AnimatedStickerNode?
private var lockBackgroundView: UIImageView?
private var lockIconView: UIImageView?
private var dismissedStillAnimationNodes: [AnimatedStickerNode] = []
private var fetchStickerDisposable: Disposable?
@ -91,11 +98,12 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
return self.staticAnimationNode.currentFrameImage != nil
}
public init(context: AccountContext, theme: PresentationTheme, item: ReactionItem, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, loopIdle: Bool, hasAppearAnimation: Bool = true, useDirectRendering: Bool = false) {
public init(context: AccountContext, theme: PresentationTheme, item: ReactionItem, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, loopIdle: Bool, isLocked: Bool, hasAppearAnimation: Bool = true, useDirectRendering: Bool = false) {
self.context = context
self.theme = theme
self.item = item
self.loopIdle = loopIdle
self.isLocked = isLocked
self.hasAppearAnimation = hasAppearAnimation
self.useDirectRendering = useDirectRendering
@ -142,6 +150,25 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
if let applicationAnimation = item.applicationAnimation {
self.fetchFullAnimationDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: .standalone(resource: applicationAnimation.resource)).start()
}
if self.isLocked {
let lockBackgroundView = UIImageView(image: lockedBackgroundImage)
self.lockBackgroundView = lockBackgroundView
self.view.addSubview(lockBackgroundView)
let lockIconView = UIImageView(image: lockedBadgeIcon)
self.lockIconView = lockIconView
self.view.addSubview(lockIconView)
if let staticAnimationNode = self.staticAnimationNode as? DefaultAnimatedStickerNodeImpl {
staticAnimationNode.frameColorUpdated = { [weak lockBackgroundView] color in
guard let lockBackgroundView else {
return
}
lockBackgroundView.tintColor = color
}
}
}
}
deinit {
@ -434,6 +461,17 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
if let customContentsNode = self.customContentsNode {
transition.updateFrame(node: customContentsNode, frame: animationFrame)
}
if let lockBackgroundView = self.lockBackgroundView, let lockIconView = self.lockIconView, let iconImage = lockIconView.image {
let lockSize: CGFloat = 12.0
let iconBackgroundFrame = CGRect(origin: CGPoint(x: animationFrame.maxX - lockSize, y: animationFrame.maxY - lockSize), size: CGSize(width: lockSize, height: lockSize))
transition.updateFrame(view: lockBackgroundView, frame: iconBackgroundFrame)
let iconFactor: CGFloat = 0.7
let iconImageSize = CGSize(width: floor(iconImage.size.width * iconFactor), height: floor(iconImage.size.height * iconFactor))
transition.updateFrame(view: lockIconView, frame: CGRect(origin: CGPoint(x: iconBackgroundFrame.minX + floorToScreenPixels((iconBackgroundFrame.width - iconImageSize.width) * 0.5), y: iconBackgroundFrame.minY + floorToScreenPixels((iconBackgroundFrame.height - iconImageSize.height) * 0.5)), size: iconImageSize))
}
}
}

View File

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

View File

@ -557,7 +557,7 @@ private class AdMessagesHistoryContextImpl {
}
let isRecommended = (flags & (1 << 5)) != 0
let displayAvatar = (flags & (1 << 6)) != 0
var displayAvatar = (flags & (1 << 6)) != 0
var target: CachedMessage.Target?
if let fromId = fromId {
@ -567,6 +567,8 @@ private class AdMessagesHistoryContextImpl {
target = .peer(fromId.peerId)
}
} else if let webPage = webPage {
displayAvatar = false
switch webPage {
case let .sponsoredWebPage(_, url, siteName, photo):
let photo = photo.flatMap { telegramMediaImageFromApiPhoto($0) }

View File

@ -18,7 +18,6 @@ public final class StoryPreloadInfo {
public let peer: PeerReference
public let storyId: Int32
public let media: EngineMedia
public let alternativeMedia: EngineMedia?
public let reactions: [MessageReaction.Reaction]
public let priority: Priority
@ -26,14 +25,12 @@ public final class StoryPreloadInfo {
peer: PeerReference,
storyId: Int32,
media: EngineMedia,
alternativeMedia: EngineMedia?,
reactions: [MessageReaction.Reaction],
priority: Priority
) {
self.peer = peer
self.storyId = storyId
self.media = media
self.alternativeMedia = alternativeMedia
self.reactions = reactions
self.priority = priority
}
@ -1062,16 +1059,19 @@ public extension TelegramEngine {
}
}
public func preloadStorySubscriptions(isHidden: Bool) -> Signal<[EngineMedia.Id: StoryPreloadInfo], NoError> {
public func preloadStorySubscriptions(isHidden: Bool, preferHighQuality: Signal<Bool, NoError>) -> Signal<[EngineMedia.Id: StoryPreloadInfo], NoError> {
let basicPeerKey = PostboxViewKey.basicPeer(self.account.peerId)
let subscriptionsKey: PostboxStorySubscriptionsKey = isHidden ? .hidden : .filtered
let storySubscriptionsKey = PostboxViewKey.storySubscriptions(key: subscriptionsKey)
return self.account.postbox.combinedView(keys: [
basicPeerKey,
storySubscriptionsKey,
PostboxViewKey.storiesState(key: .subscriptions(subscriptionsKey))
])
|> mapToSignal { views -> Signal<[EngineMedia.Id: StoryPreloadInfo], NoError> in
return combineLatest(
self.account.postbox.combinedView(keys: [
basicPeerKey,
storySubscriptionsKey,
PostboxViewKey.storiesState(key: .subscriptions(subscriptionsKey))
]),
preferHighQuality
)
|> mapToSignal { views, preferHighQuality -> Signal<[EngineMedia.Id: StoryPreloadInfo], NoError> in
guard let basicPeerView = views.views[basicPeerKey] as? BasicPeerView, let accountPeer = basicPeerView.peer else {
return .single([:])
}
@ -1178,11 +1178,17 @@ public extension TelegramEngine {
}
}
var selectedMedia: EngineMedia
if let alternativeMedia = itemAndPeer.item.alternativeMedia.flatMap(EngineMedia.init), !preferHighQuality {
selectedMedia = alternativeMedia
} else {
selectedMedia = EngineMedia(media)
}
resultResources[mediaId] = StoryPreloadInfo(
peer: peerReference,
storyId: itemAndPeer.item.id,
media: EngineMedia(media),
alternativeMedia: itemAndPeer.item.alternativeMedia.flatMap(EngineMedia.init),
media: selectedMedia,
reactions: reactions,
priority: .top(position: nextPriority)
)

View File

@ -427,6 +427,9 @@ swift_library(
"//submodules/TelegramUI/Components/MediaScrubberComponent",
"//submodules/TelegramUI/Components/Chat/ChatShareMessageTagView",
"//submodules/PromptUI",
"//submodules/Components/BundleIconComponent",
"//submodules/TelegramUI/Components/Chat/TopMessageReactions",
"//submodules/TelegramUI/Components/Chat/SavedTagNameAlertController",
] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
"//build-system:ios_sim_arm64": [],

View File

@ -1050,7 +1050,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
hasAutoremove: item.message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: item.message),
canViewReactionList: canViewMessageReactionList(message: item.message, isInline: item.associatedData.isInline),
animationCache: item.controllerInteraction.presentationContext.animationCache,
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
))
@ -1259,6 +1259,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
savedMessageTags: item.associatedData.savedMessageTags,
reactions: reactions,
message: item.message,
associatedData: item.associatedData,
accountPeer: item.associatedData.accountPeer,
isIncoming: item.message.effectivelyIncoming(item.context.account.peerId),
constrainedWidth: maxReactionsWidth
@ -1686,7 +1687,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
guard let strongSelf = weakSelf.value, let item = strongSelf.item else {
return
}
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value))
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value), false)
}
reactionButtonsNode.openReactionPreview = { gesture, sourceView, value in
guard let strongSelf = weakSelf.value, let item = strongSelf.item else {
@ -1779,7 +1780,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
f()
case let .openContextMenu(openContextMenu):
if canAddMessageReactions(message: item.message) {
item.controllerInteraction.updateMessageReaction(item.message, .default)
item.controllerInteraction.updateMessageReaction(item.message, .default, false)
} else {
item.controllerInteraction.openMessageContextMenu(openContextMenu.tapMessage, openContextMenu.selectAll, self, openContextMenu.subFrame, nil, nil)
}
@ -1788,7 +1789,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
item.controllerInteraction.clickThroughMessage()
} else if case .doubleTap = gesture {
if canAddMessageReactions(message: item.message) {
item.controllerInteraction.updateMessageReaction(item.message, .default)
item.controllerInteraction.updateMessageReaction(item.message, .default, false)
}
}
}

View File

@ -662,7 +662,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
replyCount: dateReplies,
isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread,
hasAutoremove: message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: message),
canViewReactionList: canViewMessageReactionList(message: message, isInline: associatedData.isInline),
animationCache: controllerInteraction.presentationContext.animationCache,
animationRenderer: controllerInteraction.presentationContext.animationRenderer
))
@ -1158,11 +1158,11 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
}
self.openMedia?(mode)
}
contentMedia.updateMessageReaction = { [weak controllerInteraction] message, value in
contentMedia.updateMessageReaction = { [weak controllerInteraction] message, value, force in
guard let controllerInteraction else {
return
}
controllerInteraction.updateMessageReaction(message, value)
controllerInteraction.updateMessageReaction(message, value, force)
}
contentMedia.visibility = self.visibility != .none
@ -1284,7 +1284,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
guard let self, let message = self.message else {
return
}
controllerInteraction.updateMessageReaction(message, .reaction(value))
controllerInteraction.updateMessageReaction(message, .reaction(value), false)
}
statusNode.openReactionPreview = { [weak self] gesture, sourceNode, value in

View File

@ -2128,7 +2128,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
replyCount: dateReplies,
isPinned: message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
hasAutoremove: message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: message),
canViewReactionList: canViewMessageReactionList(message: message, isInline: item.associatedData.isInline),
animationCache: item.controllerInteraction.presentationContext.animationCache,
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
))
@ -2442,6 +2442,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
savedMessageTags: item.associatedData.savedMessageTags,
reactions: bubbleReactions,
message: item.message,
associatedData: item.associatedData,
accountPeer: item.associatedData.accountPeer,
isIncoming: incoming,
constrainedWidth: maximumNodeWidth
@ -3791,7 +3792,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
guard let strongSelf = strongSelf, let item = strongSelf.item else {
return
}
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value))
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value), false)
}
reactionButtonsNode.openReactionPreview = { [weak strongSelf] gesture, sourceNode, value in
guard let strongSelf = strongSelf, let item = strongSelf.item else {
@ -3875,13 +3876,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
strongSelf.messageAccessibilityArea.frame = backgroundFrame
}
if let shareButtonNode = strongSelf.shareButtonNode {
let currentBackgroundFrame = strongSelf.backgroundNode.frame
let buttonSize = shareButtonNode.update(presentationData: item.presentationData, controllerInteraction: item.controllerInteraction, chatLocation: item.chatLocation, subject: item.associatedData.subject, message: item.message, account: item.context.account, disableComments: disablesComments)
var buttonFrame = CGRect(origin: CGPoint(x: !incoming ? currentBackgroundFrame.minX - buttonSize.width : currentBackgroundFrame.maxX + 8.0, y: currentBackgroundFrame.maxY - buttonSize.width - 1.0), size: buttonSize)
var buttonFrame = CGRect(origin: CGPoint(x: !incoming ? backgroundFrame.minX - buttonSize.width - 8.0 : backgroundFrame.maxX + 8.0, y: backgroundFrame.maxY - buttonSize.width - 1.0), size: buttonSize)
if let shareButtonOffset = shareButtonOffset {
buttonFrame.origin.x = shareButtonOffset.x
if incoming {
buttonFrame.origin.x = shareButtonOffset.x
}
buttonFrame.origin.y = buttonFrame.origin.y + shareButtonOffset.y - (buttonSize.height - 30.0)
} else if !disablesComments {
buttonFrame.origin.y = buttonFrame.origin.y - (buttonSize.height - 30.0)
@ -4046,7 +4047,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
f()
case let .openContextMenu(openContextMenu):
if canAddMessageReactions(message: openContextMenu.tapMessage) {
item.controllerInteraction.updateMessageReaction(openContextMenu.tapMessage, .default)
item.controllerInteraction.updateMessageReaction(openContextMenu.tapMessage, .default, false)
} else {
item.controllerInteraction.openMessageContextMenu(openContextMenu.tapMessage, openContextMenu.selectAll, self, openContextMenu.subFrame, nil, nil)
}
@ -4055,7 +4056,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
item.controllerInteraction.clickThroughMessage()
} else if case .doubleTap = gesture {
if canAddMessageReactions(message: item.message) {
item.controllerInteraction.updateMessageReaction(item.message, .default)
item.controllerInteraction.updateMessageReaction(item.message, .default, false)
}
}
}
@ -4743,6 +4744,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
if let selectionNode = self.selectionNode {
selectionNode.updateSelected(selected, animated: animated)
let selectionFrame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.contentSize.width, height: self.contentSize.height))
selectionNode.frame = selectionFrame
selectionNode.updateLayout(size: selectionFrame.size, leftInset: self.safeInsets.left)
self.subnodeTransform = CATransform3DMakeTranslation(offset, 0.0, 0.0);

View File

@ -59,7 +59,7 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
guard let strongSelf = self, let item = strongSelf.item else {
return
}
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value))
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value), false)
}
self.dateAndStatusNode.openReactionPreview = { [weak self] gesture, sourceView, value in
@ -294,7 +294,7 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread,
hasAutoremove: item.message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: item.message),
canViewReactionList: canViewMessageReactionList(message: item.message, isInline: item.associatedData.isInline),
animationCache: item.controllerInteraction.presentationContext.animationCache,
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
))

View File

@ -394,6 +394,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
selectedForeground: themeColors.reactionActiveForeground.argb,
extractedBackground: arguments.presentationData.theme.theme.contextMenu.backgroundColor.argb,
extractedForeground: arguments.presentationData.theme.theme.contextMenu.primaryColor.argb,
extractedSelectedForeground: arguments.presentationData.theme.theme.contextMenu.primaryColor.argb,
deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb,
selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb
)
@ -406,7 +407,8 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
deselectedForeground: themeColors.reactionInactiveForeground.argb,
selectedForeground: themeColors.reactionActiveForeground.argb,
extractedBackground: arguments.presentationData.theme.theme.contextMenu.backgroundColor.argb,
extractedForeground: arguments.presentationData.theme.theme.contextMenu.primaryColor.argb,
extractedForeground: arguments.presentationData.theme.theme.contextMenu.primaryColor.argb,
extractedSelectedForeground: arguments.presentationData.theme.theme.list.itemCheckColors.foregroundColor.argb,
deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb,
selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb
)
@ -755,8 +757,6 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
)
case let .trailingContent(contentWidth, reactionSettings):
if let reactionSettings = reactionSettings, !reactionSettings.displayInline {
let isTag = arguments.areReactionsTags
var totalReactionCount: Int = 0
for reaction in arguments.reactions {
totalReactionCount += Int(reaction.count)
@ -768,11 +768,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
guard let strongSelf = self else {
return
}
if isTag {
strongSelf.openReactionPreview?(nil, itemNode.containerView, value)
} else {
strongSelf.reactionSelected?(itemNode, value)
}
strongSelf.reactionSelected?(itemNode, value)
},
reactions: arguments.reactions.map { reaction in
var centerAnimation: TelegramMediaFile?

View File

@ -68,7 +68,7 @@ public class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
guard let strongSelf = self, let item = strongSelf.item else {
return
}
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value))
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value), false)
}
self.interactiveFileNode.dateAndStatusNode.openReactionPreview = { [weak self] gesture, sourceNode, value in

View File

@ -134,7 +134,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode,
guard let strongSelf = self, let item = strongSelf.item else {
return
}
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value))
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value), false)
}
self.dateAndStatusNode.openReactionPreview = { [weak self] gesture, sourceView, value in
@ -524,7 +524,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode,
replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread,
hasAutoremove: item.message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: item.message),
canViewReactionList: canViewMessageReactionList(message: item.message, isInline: item.associatedData.isInline),
animationCache: item.controllerInteraction.presentationContext.animationCache,
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
))

View File

@ -148,7 +148,7 @@ public class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentN
guard let strongSelf = self, let item = strongSelf.item else {
return
}
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value))
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value), false)
}
self.interactiveFileNode.dateAndStatusNode.openReactionPreview = { [weak self] gesture, sourceNode, value in

View File

@ -613,6 +613,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureReco
savedMessageTags: item.associatedData.savedMessageTags,
reactions: reactions,
message: item.message,
associatedData: item.associatedData,
accountPeer: item.associatedData.accountPeer,
isIncoming: item.message.effectivelyIncoming(item.context.account.peerId),
constrainedWidth: maxReactionsWidth
@ -833,7 +834,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureReco
guard let strongSelf = weakSelf.value, let item = strongSelf.item else {
return
}
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value))
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value), false)
}
reactionButtonsNode.openReactionPreview = { gesture, sourceNode, value in
guard let strongSelf = weakSelf.value, let item = strongSelf.item else {

View File

@ -941,7 +941,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode {
replyCount: dateReplies,
isPinned: arguments.isPinned && !arguments.associatedData.isInPinnedListMode,
hasAutoremove: arguments.message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: arguments.message),
canViewReactionList: canViewMessageReactionList(message: arguments.message, isInline: arguments.associatedData.isInline),
animationCache: arguments.controllerInteraction.presentationContext.animationCache,
animationRenderer: arguments.controllerInteraction.presentationContext.animationRenderer
))

View File

@ -582,7 +582,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
hasAutoremove: item.message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: item.message),
canViewReactionList: canViewMessageReactionList(message: item.message, isInline: item.associatedData.isInline),
animationCache: item.controllerInteraction.presentationContext.animationCache,
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
))

View File

@ -455,7 +455,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
public var activateLocalContent: (InteractiveMediaNodeActivateContent) -> Void = { _ in }
public var activatePinch: ((PinchSourceContainerNode) -> Void)?
public var updateMessageReaction: ((Message, ChatControllerInteractionReaction) -> Void)?
public var updateMessageReaction: ((Message, ChatControllerInteractionReaction, Bool) -> Void)?
override public init() {
self.pinchContainerNode = PinchSourceContainerNode()
@ -877,7 +877,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
replyCount: dateAndStatus.dateReplies,
isPinned: dateAndStatus.isPinned,
hasAutoremove: message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: message),
canViewReactionList: canViewMessageReactionList(message: message, isInline: associatedData.isInline),
animationCache: presentationContext.animationCache,
animationRenderer: presentationContext.animationRenderer
))

View File

@ -158,7 +158,11 @@ public struct ChatMessageItemLayoutConstants {
}
}
public func canViewMessageReactionList(message: Message) -> Bool {
public func canViewMessageReactionList(message: Message, isInline: Bool) -> Bool {
if isInline {
return false
}
var found = false
var canViewList = false
for attribute in message.attributes {

View File

@ -281,7 +281,7 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
hasAutoremove: item.message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: item.message),
canViewReactionList: canViewMessageReactionList(message: item.message, isInline: item.associatedData.isInline),
animationCache: item.controllerInteraction.presentationContext.animationCache,
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
))

View File

@ -58,11 +58,11 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
}
}
self.interactiveImageNode.updateMessageReaction = { [weak self] message, value in
self.interactiveImageNode.updateMessageReaction = { [weak self] message, value, force in
guard let strongSelf = self, let item = strongSelf.item else {
return
}
item.controllerInteraction.updateMessageReaction(message, value)
item.controllerInteraction.updateMessageReaction(message, value, force)
}
self.interactiveImageNode.activatePinch = { [weak self] sourceNode in

View File

@ -1029,7 +1029,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
hasAutoremove: item.message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: item.message),
canViewReactionList: canViewMessageReactionList(message: item.message, isInline: item.associatedData.isInline),
animationCache: item.controllerInteraction.presentationContext.animationCache,
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
))

View File

@ -62,6 +62,7 @@ public final class MessageReactionButtonsNode: ASDisplayNode {
reactions: ReactionsMessageAttribute,
accountPeer: EnginePeer?,
message: Message,
associatedData: ChatMessageItemAssociatedData,
alignment: DisplayAlignment,
constrainedWidth: CGFloat,
type: DisplayType
@ -77,7 +78,8 @@ public final class MessageReactionButtonsNode: ASDisplayNode {
deselectedForeground: themeColors.reactionInactiveForeground.argb,
selectedForeground: themeColors.reactionActiveForeground.argb,
extractedBackground: presentationData.theme.theme.contextMenu.backgroundColor.argb,
extractedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb,
extractedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb,
extractedSelectedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb,
deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb,
selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb
)
@ -89,7 +91,8 @@ public final class MessageReactionButtonsNode: ASDisplayNode {
deselectedForeground: themeColors.reactionInactiveForeground.argb,
selectedForeground: themeColors.reactionActiveForeground.argb,
extractedBackground: presentationData.theme.theme.contextMenu.backgroundColor.argb,
extractedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb,
extractedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb,
extractedSelectedForeground: presentationData.theme.theme.list.itemCheckColors.foregroundColor.argb,
deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb,
selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb
)
@ -106,7 +109,8 @@ public final class MessageReactionButtonsNode: ASDisplayNode {
deselectedForeground: themeColors.reactionInactiveForeground.argb,
selectedForeground: themeColors.reactionActiveForeground.argb,
extractedBackground: presentationData.theme.theme.contextMenu.backgroundColor.argb,
extractedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb,
extractedForeground: presentationData.theme.theme.contextMenu.primaryColor.argb,
extractedSelectedForeground: presentationData.theme.theme.list.itemCheckColors.foregroundColor.argb,
deselectedMediaPlaceholder: themeColors.reactionInactiveMediaPlaceholder.argb,
selectedMediaPlaceholder: themeColors.reactionActiveMediaPlaceholder.argb
)
@ -350,7 +354,7 @@ public final class MessageReactionButtonsNode: ASDisplayNode {
let itemValue = item.value
let itemNode = item.node
item.node.view.isGestureEnabled = true
let canViewReactionList = canViewMessageReactionList(message: message)
let canViewReactionList = canViewMessageReactionList(message: message, isInline: associatedData.isInline)
item.node.view.activateAfterCompletion = !canViewReactionList
item.node.view.activated = { [weak itemNode] gesture, _ in
guard let strongSelf = self, let itemNode = itemNode else {
@ -490,7 +494,7 @@ public final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleConte
guard let strongSelf = self, let item = strongSelf.item else {
return
}
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value))
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value), false)
}
self.buttonsNode.openReactionPreview = { [weak self] gesture, sourceNode, value in
@ -526,7 +530,7 @@ public final class ChatMessageReactionsFooterContentNode: ChatMessageBubbleConte
context: item.context,
presentationData: item.presentationData,
presentationContext: item.controllerInteraction.presentationContext,
availableReactions: item.associatedData.availableReactions, savedMessageTags: item.associatedData.savedMessageTags, reactions: reactionsAttribute, accountPeer: item.associatedData.accountPeer, message: item.message, alignment: .left, constrainedWidth: constrainedSize.width - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, type: item.message.effectivelyIncoming(item.context.account.peerId) ? .incoming : .outgoing)
availableReactions: item.associatedData.availableReactions, savedMessageTags: item.associatedData.savedMessageTags, reactions: reactionsAttribute, accountPeer: item.associatedData.accountPeer, message: item.message, associatedData: item.associatedData, alignment: .left, constrainedWidth: constrainedSize.width - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, type: item.message.effectivelyIncoming(item.context.account.peerId) ? .incoming : .outgoing)
return (layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right + buttonsUpdate.proposedWidth, { boundingWidth in
var boundingSize = CGSize()
@ -607,6 +611,7 @@ public final class ChatMessageReactionButtonsNode: ASDisplayNode {
public let savedMessageTags: SavedMessageTags?
public let reactions: ReactionsMessageAttribute
public let message: Message
public let associatedData: ChatMessageItemAssociatedData
public let accountPeer: EnginePeer?
public let isIncoming: Bool
public let constrainedWidth: CGFloat
@ -619,6 +624,7 @@ public final class ChatMessageReactionButtonsNode: ASDisplayNode {
savedMessageTags: SavedMessageTags?,
reactions: ReactionsMessageAttribute,
message: Message,
associatedData: ChatMessageItemAssociatedData,
accountPeer: EnginePeer?,
isIncoming: Bool,
constrainedWidth: CGFloat
@ -630,6 +636,7 @@ public final class ChatMessageReactionButtonsNode: ASDisplayNode {
self.savedMessageTags = savedMessageTags
self.reactions = reactions
self.message = message
self.associatedData = associatedData
self.accountPeer = accountPeer
self.isIncoming = isIncoming
self.constrainedWidth = constrainedWidth
@ -670,6 +677,7 @@ public final class ChatMessageReactionButtonsNode: ASDisplayNode {
reactions: arguments.reactions,
accountPeer: arguments.accountPeer,
message: arguments.message,
associatedData: arguments.associatedData,
alignment: arguments.isIncoming ? .left : .right,
constrainedWidth: arguments.constrainedWidth,
type: .freeform

View File

@ -137,7 +137,7 @@ public class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNod
replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread,
hasAutoremove: item.message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: item.message),
canViewReactionList: canViewMessageReactionList(message: item.message, isInline: item.associatedData.isInline),
animationCache: item.controllerInteraction.presentationContext.animationCache,
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
))

View File

@ -19,6 +19,9 @@ swift_library(
"//submodules/AppBundle",
"//submodules/ChatPresentationInterfaceState",
"//submodules/TelegramUI/Components/Chat/ChatInputPanelNode",
"//submodules/TelegramUI/Components/EntityKeyboard",
"//submodules/TelegramUI/Components/Chat/TopMessageReactions",
"//submodules/ReactionSelectionNode",
],
visibility = [
"//visibility:public",

View File

@ -10,14 +10,68 @@ import AccountContext
import AppBundle
import ChatPresentationInterfaceState
import ChatInputPanelNode
import ReactionSelectionNode
import EntityKeyboard
import TopMessageReactions
private final class ChatMessageSelectionInputPanelNodeViewForOverlayContent: UIView, ChatInputPanelViewForOverlayContent {
var reactionContextNode: ReactionContextNode?
var anchorRect: CGRect?
override init(frame: CGRect) {
super.init(frame: frame)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.backgroundTapGesture(_:))))
}
required init(coder: NSCoder) {
preconditionFailure()
}
@objc private func backgroundTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.dismissReactionSelection()
}
}
func dismissReactionSelection() {
if let reactionContextNode = self.reactionContextNode {
self.reactionContextNode = nil
reactionContextNode.animateOut(to: self.anchorRect, animatingOutToReaction: false)
ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut).updateAlpha(node: reactionContextNode, alpha: 0.0, completion: { [weak reactionContextNode] _ in
reactionContextNode?.removeFromSupernode()
})
}
}
func maybeDismissContent(point: CGPoint) {
if self.hitTest(point, with: nil) == self {
self.dismissReactionSelection()
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let reactionContextNode = self.reactionContextNode {
if let result = reactionContextNode.view.hitTest(self.convert(point, to: reactionContextNode.view), with: event) {
return result
}
return self
}
return nil
}
}
public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
private let deleteButton: HighlightableButtonNode
private let reportButton: HighlightableButtonNode
private let forwardButton: HighlightableButtonNode
private let shareButton: HighlightableButtonNode
private let tagButton: HighlightableButtonNode
private let tagEditButton: HighlightableButtonNode
private let separatorNode: ASDisplayNode
private let reactionOverlayContainer: ChatMessageSelectionInputPanelNodeViewForOverlayContent
private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, metrics: LayoutMetrics, isSecondary: Bool, isMediaInputExpanded: Bool)?
private var presentationInterfaceState: ChatPresentationInterfaceState?
private var actions: ChatAvailableMessageActions?
@ -30,25 +84,7 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
public var selectedMessages = Set<MessageId>() {
didSet {
if oldValue != self.selectedMessages {
self.forwardButton.isEnabled = self.selectedMessages.count != 0
if self.selectedMessages.isEmpty {
self.actions = nil
if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded) = self.validLayout, let interfaceState = self.presentationInterfaceState {
let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: interfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded)
}
self.canDeleteMessagesDisposable.set(nil)
} else if let context = self.context {
self.canDeleteMessagesDisposable.set((context.sharedContext.chatAvailableMessageActions(engine: context.engine, accountPeerId: context.account.peerId, messageIds: self.selectedMessages)
|> deliverOnMainQueue).startStrict(next: { [weak self] actions in
if let strongSelf = self {
strongSelf.actions = actions
if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded) = strongSelf.validLayout, let interfaceState = strongSelf.presentationInterfaceState {
let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: interfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded)
}
}
}))
}
self.updateActions()
}
}
}
@ -75,6 +111,16 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
self.shareButton.isAccessibilityElement = true
self.shareButton.accessibilityLabel = strings.VoiceOver_MessageContextShare
self.tagButton = HighlightableButtonNode(pointerStyle: .rectangle(CGSize(width: 56.0, height: 40.0)))
self.tagButton.isAccessibilityElement = true
//TODO:localize
self.tagButton.accessibilityLabel = "Tag"
self.tagEditButton = HighlightableButtonNode(pointerStyle: .rectangle(CGSize(width: 56.0, height: 40.0)))
self.tagEditButton.isAccessibilityElement = true
//TODO:localize
self.tagEditButton.accessibilityLabel = "Edit Tag"
self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled])
self.reportButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionReport"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
@ -83,18 +129,26 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled])
self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled])
self.tagButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TagIcon"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
self.tagEditButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TagEditIcon"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
self.separatorNode = ASDisplayNode()
self.separatorNode.backgroundColor = theme.chat.inputPanel.panelSeparatorColor
self.reactionOverlayContainer = ChatMessageSelectionInputPanelNodeViewForOverlayContent()
super.init()
self.addSubnode(self.deleteButton)
self.addSubnode(self.reportButton)
self.addSubnode(self.forwardButton)
self.addSubnode(self.shareButton)
self.addSubnode(self.tagButton)
self.addSubnode(self.tagEditButton)
self.addSubnode(self.separatorNode)
self.viewForOverlayContent = self.reactionOverlayContainer
self.forwardButton.isImplicitlyDisabled = true
self.shareButton.isImplicitlyDisabled = true
@ -102,12 +156,36 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
self.reportButton.addTarget(self, action: #selector(self.reportButtonPressed), forControlEvents: .touchUpInside)
self.forwardButton.addTarget(self, action: #selector(self.forwardButtonPressed), forControlEvents: .touchUpInside)
self.shareButton.addTarget(self, action: #selector(self.shareButtonPressed), forControlEvents: .touchUpInside)
self.tagButton.addTarget(self, action: #selector(self.tagButtonPressed), forControlEvents: .touchUpInside)
self.tagEditButton.addTarget(self, action: #selector(self.tagButtonPressed), forControlEvents: .touchUpInside)
}
deinit {
self.canDeleteMessagesDisposable.dispose()
}
private func updateActions() {
self.forwardButton.isEnabled = self.selectedMessages.count != 0
if self.selectedMessages.isEmpty {
self.actions = nil
if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded) = self.validLayout, let interfaceState = self.presentationInterfaceState {
let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: interfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded)
}
self.canDeleteMessagesDisposable.set(nil)
} else if let context = self.context {
self.canDeleteMessagesDisposable.set((context.sharedContext.chatAvailableMessageActions(engine: context.engine, accountPeerId: context.account.peerId, messageIds: self.selectedMessages, keepUpdated: true)
|> deliverOnMainQueue).startStrict(next: { [weak self] actions in
if let strongSelf = self {
strongSelf.actions = actions
if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded) = strongSelf.validLayout, let interfaceState = strongSelf.presentationInterfaceState {
let _ = strongSelf.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .immediate, interfaceState: interfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded)
}
}
}))
}
}
public func updateTheme(theme: PresentationTheme) {
if self.theme !== theme {
self.theme = theme
@ -120,6 +198,8 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled])
self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
self.shareButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionAction"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled])
self.tagButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/WebpageIcon"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
self.tagEditButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/LinkSettingsIcon"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal])
self.separatorNode.backgroundColor = theme.chat.inputPanel.panelSeparatorColor
}
@ -155,6 +235,120 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
}
}
@objc private func tagButtonPressed() {
guard let context = self.context else {
return
}
if self.reactionOverlayContainer.reactionContextNode != nil {
return
}
let reactionItems: Signal<[ReactionItem], NoError> = tagMessageReactions(context: context)
let _ = (reactionItems
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] reactionItems in
guard let self, let actions = self.actions, let context = self.context else {
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize
let reactionContextNode = ReactionContextNode(
context: context,
animationCache: context.animationCache,
presentationData: presentationData,
items: reactionItems.map(ReactionContextItem.reaction),
selectedItems: actions.editTags,
title: actions.editTags.isEmpty ? "Tag a message with emojis for quick search" : "Edit tags of selected messages",
reactionsLocked: false,
alwaysAllowPremiumReactions: false,
allPresetReactionsAreAvailable: true,
getEmojiContent: { animationCache, animationRenderer in
let mappedReactionItems: [EmojiComponentReactionItem] = reactionItems.map { reaction -> EmojiComponentReactionItem in
return EmojiComponentReactionItem(reaction: reaction.reaction.rawValue, file: reaction.stillAnimation)
}
return EmojiPagerContentComponent.emojiInputData(
context: context,
animationCache: animationCache,
animationRenderer: animationRenderer,
isStandalone: false,
subject: .messageTag,
hasTrending: false,
topReactionItems: mappedReactionItems,
areUnicodeEmojiEnabled: false,
areCustomEmojiEnabled: true,
chatPeerId: context.account.peerId,
selectedItems: Set(),
premiumIfSavedMessages: false
)
},
isExpandedUpdated: { [weak self] transition in
guard let self else {
return
}
self.update(transition: transition)
},
requestLayout: { [weak self] transition in
guard let self else {
return
}
self.update(transition: transition)
},
requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in
guard let self else {
return
}
self.update(transition: transition)
}
)
reactionContextNode.reactionSelected = { [weak self] updateReaction, _ in
guard let self, let context = self.context, let presentationInterfaceState = self.presentationInterfaceState, let actions = self.actions else {
return
}
var reactions = actions.editTags
if reactions.contains(updateReaction.reaction) {
reactions.remove(updateReaction.reaction)
} else {
reactions.insert(updateReaction.reaction)
}
let mappedUpdatedReactions = reactions.map { reaction -> UpdateMessageReaction in
switch reaction {
case let .builtin(value):
return .builtin(value)
case let .custom(fileId):
return .custom(fileId: fileId, file: nil)
}
}
if let selectionState = presentationInterfaceState.interfaceState.selectionState {
for id in selectionState.selectedIds {
context.engine.messages.setMessageReactions(id: id, reactions: mappedUpdatedReactions)
}
}
self.reactionOverlayContainer.dismissReactionSelection()
}
reactionContextNode.displayTail = true
reactionContextNode.forceTailToRight = true
reactionContextNode.forceDark = false
self.reactionOverlayContainer.reactionContextNode = reactionContextNode
self.reactionOverlayContainer.addSubnode(reactionContextNode)
self.update(transition: .immediate)
})
}
private func update(transition: ContainedViewLayoutTransition) {
if let (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded) = self.validLayout, let interfaceState = self.presentationInterfaceState {
let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: transition, interfaceState: interfaceState, metrics: metrics, isMediaInputExpanded: isMediaInputExpanded)
}
}
override public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat {
self.validLayout = (width, leftInset, rightInset, bottomInset, additionalSideInsets, maxHeight, metrics, isSecondary, isMediaInputExpanded)
@ -182,6 +376,19 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
self.deleteButton.isHidden = false
}
self.reportButton.isHidden = !self.reportButton.isEnabled
if actions.setTag {
if !actions.editTags.isEmpty {
self.tagButton.isHidden = true
self.tagEditButton.isHidden = false
} else {
self.tagButton.isHidden = false
self.tagEditButton.isHidden = true
}
} else {
self.tagButton.isHidden = true
self.tagEditButton.isHidden = true
}
} else {
self.deleteButton.isEnabled = false
self.deleteButton.isHidden = self.peerMedia
@ -189,6 +396,10 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
self.reportButton.isHidden = true
self.forwardButton.isImplicitlyDisabled = true
self.shareButton.isImplicitlyDisabled = true
self.tagButton.isHidden = true
self.tagEditButton.isHidden = true
self.tagButton.isHidden = true
self.tagEditButton.isHidden = true
}
if self.reportButton.isHidden || (self.peerMedia && self.deleteButton.isHidden && self.reportButton.isHidden) {
@ -204,41 +415,96 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
width -= additionalSideInsets.right
}
var tagButton: HighlightableButtonNode?
if !self.tagButton.isHidden {
tagButton = self.tagButton
} else if !self.tagEditButton.isHidden {
tagButton = self.tagEditButton
}
let buttons: [HighlightableButtonNode]
if self.reportButton.isHidden {
self.deleteButton.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: 57.0, height: panelHeight))
self.forwardButton.frame = CGRect(origin: CGPoint(x: width - rightInset - 57.0, y: 0.0), size: CGSize(width: 57.0, height: panelHeight))
self.shareButton.frame = CGRect(origin: CGPoint(x: floor((width - rightInset - 57.0) / 2.0), y: 0.0), size: CGSize(width: 57.0, height: panelHeight))
if let tagButton {
buttons = [
self.deleteButton,
self.forwardButton,
tagButton,
self.shareButton
]
} else {
buttons = [
self.deleteButton,
self.forwardButton,
self.shareButton
]
}
} else if !self.deleteButton.isHidden {
let buttons: [HighlightableButtonNode] = [
self.deleteButton,
self.reportButton,
self.shareButton,
self.forwardButton
]
let buttonSize = CGSize(width: 57.0, height: panelHeight)
let availableWidth = width - leftInset - rightInset
let spacing: CGFloat = floor((availableWidth - buttonSize.width * CGFloat(buttons.count)) / CGFloat(buttons.count - 1))
var offset: CGFloat = leftInset
for i in 0 ..< buttons.count {
let button = buttons[i]
if i == buttons.count - 1 {
button.frame = CGRect(origin: CGPoint(x: width - rightInset - buttonSize.width, y: 0.0), size: buttonSize)
} else {
button.frame = CGRect(origin: CGPoint(x: offset, y: 0.0), size: buttonSize)
}
offset += buttonSize.width + spacing
if let tagButton {
buttons = [
self.deleteButton,
self.reportButton,
tagButton,
self.shareButton,
self.forwardButton
]
} else {
buttons = [
self.deleteButton,
self.reportButton,
self.shareButton,
self.forwardButton
]
}
} else {
self.deleteButton.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: 53.0, height: panelHeight))
self.forwardButton.frame = CGRect(origin: CGPoint(x: width - rightInset - 57.0, y: 0.0), size: CGSize(width: 57.0, height: panelHeight))
self.reportButton.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: 53.0, height: 47.0))
self.shareButton.frame = CGRect(origin: CGPoint(x: floor((width - rightInset - 57.0) / 2.0), y: 0.0), size: CGSize(width: 57.0, height: panelHeight))
if let tagButton {
buttons = [
self.deleteButton,
self.forwardButton,
self.reportButton,
tagButton,
self.shareButton
]
} else {
buttons = [
self.deleteButton,
self.forwardButton,
self.reportButton,
self.shareButton
]
}
}
let buttonSize = CGSize(width: 57.0, height: panelHeight)
let availableWidth = width - leftInset - rightInset
let spacing: CGFloat = floor((availableWidth - buttonSize.width * CGFloat(buttons.count)) / CGFloat(buttons.count - 1))
var offset: CGFloat = leftInset
for i in 0 ..< buttons.count {
let button = buttons[i]
if i == buttons.count - 1 {
button.frame = CGRect(origin: CGPoint(x: width - rightInset - buttonSize.width, y: 0.0), size: buttonSize)
} else {
button.frame = CGRect(origin: CGPoint(x: offset, y: 0.0), size: buttonSize)
}
offset += buttonSize.width + spacing
}
transition.updateAlpha(node: self.separatorNode, alpha: isSecondary ? 1.0 : 0.0)
self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: panelHeight), size: CGSize(width: width, height: UIScreenPixel))
if let reactionContextNode = self.reactionOverlayContainer.reactionContextNode, let tagButton {
let isFirstTime = reactionContextNode.bounds.isEmpty
let size = CGSize(width: width, height: maxHeight)
let reactionsAnchorRect = tagButton.frame.offsetBy(dx: -54.0, dy: -(panelHeight - size.height) + 14.0)
transition.updateFrame(node: reactionContextNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelHeight - size.height), size: size))
reactionContextNode.updateLayout(size: size, insets: UIEdgeInsets(), anchorRect: reactionsAnchorRect, centerAligned: true, isCoveredByInput: false, isAnimatingOut: false, transition: transition)
reactionContextNode.updateIsIntersectingContent(isIntersectingContent: true, transition: .immediate)
if isFirstTime {
reactionContextNode.animateIn(from: reactionsAnchorRect)
}
}
return panelHeight
}

View File

@ -633,7 +633,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
hasAutoremove: item.message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: item.message),
canViewReactionList: canViewMessageReactionList(message: item.message, isInline: item.associatedData.isInline),
animationCache: item.controllerInteraction.presentationContext.animationCache,
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
))
@ -842,6 +842,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
savedMessageTags: item.associatedData.savedMessageTags,
reactions: reactions,
message: item.message,
associatedData: item.associatedData,
accountPeer: item.associatedData.accountPeer,
isIncoming: item.message.effectivelyIncoming(item.context.account.peerId),
constrainedWidth: maxReactionsWidth
@ -1233,7 +1234,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
guard let strongSelf = weakSelf.value, let item = strongSelf.item else {
return
}
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value))
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value), false)
}
reactionButtonsNode.openReactionPreview = { gesture, sourceNode, value in
guard let strongSelf = weakSelf.value, let item = strongSelf.item else {
@ -1332,7 +1333,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
f()
case let .openContextMenu(openContextMenu):
if canAddMessageReactions(message: item.message) {
item.controllerInteraction.updateMessageReaction(openContextMenu.tapMessage, .default)
item.controllerInteraction.updateMessageReaction(openContextMenu.tapMessage, .default, false)
} else {
item.controllerInteraction.openMessageContextMenu(openContextMenu.tapMessage, openContextMenu.selectAll, self, openContextMenu.subFrame, nil, nil)
}

View File

@ -578,7 +578,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
replyCount: dateReplies,
isPinned: item.message.tags.contains(.pinned) && (!item.associatedData.isInPinnedListMode || isReplyThread),
hasAutoremove: item.message.isSelfExpiring,
canViewReactionList: canViewMessageReactionList(message: item.message),
canViewReactionList: canViewMessageReactionList(message: item.message, isInline: item.associatedData.isInline),
animationCache: item.controllerInteraction.presentationContext.animationCache,
animationRenderer: item.controllerInteraction.presentationContext.animationRenderer
))
@ -719,7 +719,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
guard let strongSelf, let item = strongSelf.item else {
return
}
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value))
item.controllerInteraction.updateMessageReaction(item.message, .reaction(value), false)
}
statusNode.openReactionPreview = { [weak strongSelf] gesture, sourceNode, value in
guard let strongSelf, let item = strongSelf.item else {

View File

@ -271,7 +271,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
let gesture: ContextGesture? = anyRecognizer as? ContextGesture
self?.openMessageContextMenu(message: message, selectAll: selectAll, node: node, frame: frame, recognizer: recognizer, gesture: gesture, location: location)
}, openMessageReactionContextMenu: { _, _, _, _ in
}, updateMessageReaction: { _, _ in
}, updateMessageReaction: { _, _, _ in
}, activateMessagePinch: { _ in
}, openMessageContextActions: { _, _, _, _ in
}, navigateToMessage: { _, _, _ in }, navigateToMessageStandalone: { _ in

View File

@ -34,6 +34,7 @@ public final class ChatShareMessageTagView: UIView, UndoOverlayControllerAdditio
items: reactionItems.map(ReactionContextItem.reaction),
selectedItems: Set(),
title: presentationData.strings.Chat_ContextMenuTagsTitle,
reactionsLocked: false,
alwaysAllowPremiumReactions: false,
allPresetReactionsAreAvailable: true,
getEmojiContent: { animationCache, animationRenderer in

View File

@ -0,0 +1,28 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "SavedTagNameAlertController",
module_name = "SavedTagNameAlertController",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/ComponentFlow",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BalancedTextComponent",
"//submodules/TelegramUI/Components/EmojiStatusComponent",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,557 @@
import Foundation
import UIKit
import SwiftSignalKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import TelegramPresentationData
import AccountContext
import ComponentFlow
import MultilineTextComponent
import BalancedTextComponent
import EmojiStatusComponent
private final class PromptInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate {
private var theme: PresentationTheme
private let backgroundNode: ASImageNode
private let textInputNode: EditableTextNode
private let placeholderNode: ASTextNode
private let characterLimitView = ComponentView<Empty>()
private let characterLimit: Int
var updateHeight: (() -> Void)?
var complete: (() -> Void)?
var textChanged: ((String) -> Void)?
private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0)
private let inputInsets: UIEdgeInsets
var text: String {
get {
return self.textInputNode.attributedText?.string ?? ""
}
set {
self.textInputNode.attributedText = NSAttributedString(string: newValue, font: Font.regular(13.0), textColor: self.theme.actionSheet.inputTextColor)
self.placeholderNode.isHidden = !newValue.isEmpty
}
}
var placeholder: String = "" {
didSet {
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(13.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
}
}
init(theme: PresentationTheme, placeholder: String, characterLimit: Int) {
self.theme = theme
self.characterLimit = characterLimit
self.inputInsets = UIEdgeInsets(top: 9.0, left: 17.0, bottom: 9.0, right: 16.0)
self.backgroundNode = ASImageNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.displaysAsynchronously = false
self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 16.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0)
self.textInputNode = EditableTextNode()
self.textInputNode.typingAttributes = [NSAttributedString.Key.font.rawValue: Font.regular(13.0), NSAttributedString.Key.foregroundColor.rawValue: theme.actionSheet.inputTextColor]
self.textInputNode.clipsToBounds = true
self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
self.textInputNode.textContainerInset = UIEdgeInsets(top: self.inputInsets.top, left: self.inputInsets.left, bottom: self.inputInsets.bottom, right: self.inputInsets.right)
self.textInputNode.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
self.textInputNode.keyboardType = .default
self.textInputNode.autocapitalizationType = .sentences
self.textInputNode.returnKeyType = .done
self.textInputNode.autocorrectionType = .default
self.textInputNode.tintColor = theme.actionSheet.controlAccentColor
self.placeholderNode = ASTextNode()
self.placeholderNode.isUserInteractionEnabled = false
self.placeholderNode.displaysAsynchronously = false
self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(13.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
super.init()
self.textInputNode.delegate = self
self.addSubnode(self.backgroundNode)
self.addSubnode(self.textInputNode)
self.addSubnode(self.placeholderNode)
}
func updateTheme(_ theme: PresentationTheme) {
self.theme = theme
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 16.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0)
self.textInputNode.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
self.textInputNode.tintColor = self.theme.actionSheet.controlAccentColor
}
func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
let backgroundInsets = self.backgroundInsets
let inputInsets = self.inputInsets
let textFieldHeight = self.calculateTextFieldMetrics(width: width)
let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom
let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom))
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
let placeholderSize = self.placeholderNode.measure(backgroundFrame.size)
transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left + 17.0, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize))
transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right, height: backgroundFrame.size.height)))
let characterLimitString: String
let characterLimitColor: UIColor
if self.text.count <= self.characterLimit {
let remaining = self.characterLimit - self.text.count
if remaining < 5 {
characterLimitString = "\(remaining)"
} else {
characterLimitString = " "
}
characterLimitColor = self.theme.list.itemPlaceholderTextColor
} else {
characterLimitString = "\(self.characterLimit - self.text.count)"
characterLimitColor = self.theme.list.itemDestructiveColor
}
let characterLimitSize = self.characterLimitView.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: characterLimitString, font: Font.regular(13.0), textColor: characterLimitColor))
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
if let characterLimitComponentView = self.characterLimitView.view {
if characterLimitComponentView.superview == nil {
self.view.addSubview(characterLimitComponentView)
}
characterLimitComponentView.frame = CGRect(origin: CGPoint(x: width - 23.0 - characterLimitSize.width, y: 18.0), size: characterLimitSize)
}
return panelHeight
}
func activateInput() {
self.textInputNode.becomeFirstResponder()
}
func deactivateInput() {
self.textInputNode.resignFirstResponder()
}
@objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
self.updateTextNodeText(animated: true)
self.textChanged?(editableTextNode.textView.text)
self.placeholderNode.isHidden = !(editableTextNode.textView.text ?? "").isEmpty
}
func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
/*let currentText = (editableTextNode.attributedText?.string ?? "") as NSString
var resultText = currentText.replacingCharacters(in: range, with: text)
if resultText.count > self.characterLimit {
resultText = String(resultText[resultText.startIndex ..< resultText.index(resultText.startIndex, offsetBy: self.characterLimit)])
editableTextNode.attributedText = NSAttributedString(string: resultText, font: Font.regular(13.0), textColor: self.theme.actionSheet.inputTextColor)
return false
}*/
if text == "\n" {
self.complete?()
return false
}
return true
}
private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat {
let backgroundInsets = self.backgroundInsets
let inputInsets = self.inputInsets
let unboundTextFieldHeight = max(34.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right, height: CGFloat.greatestFiniteMagnitude)).height))
return min(61.0, max(34.0, unboundTextFieldHeight))
}
private func updateTextNodeText(animated: Bool) {
let backgroundInsets = self.backgroundInsets
let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width)
let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom
if !self.bounds.size.height.isEqual(to: panelHeight) {
self.updateHeight?()
}
}
@objc func clearPressed() {
self.textInputNode.attributedText = nil
self.deactivateInput()
}
}
private final class SavedTagNameAlertContentNode: AlertContentNode {
private let context: AccountContext
private var theme: AlertControllerTheme
private let strings: PresentationStrings
private let text: String
private let subtext: String
private let titleFont: PromptControllerTitleFont
private let reaction: MessageReaction.Reaction
private let file: TelegramMediaFile
private let textView = ComponentView<Empty>()
private let subtextView = ComponentView<Empty>()
private let iconView = ComponentView<Empty>()
let inputFieldNode: PromptInputFieldNode
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private let disposable = MetaDisposable()
private var validLayout: CGSize?
private let hapticFeedback = HapticFeedback()
var complete: (() -> Void)? {
didSet {
self.inputFieldNode.complete = self.complete
}
}
override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], text: String, subtext: String, reaction: MessageReaction.Reaction, file: TelegramMediaFile, titleFont: PromptControllerTitleFont, value: String?, characterLimit: Int) {
self.context = context
self.theme = theme
self.strings = strings
self.text = text
self.subtext = subtext
self.reaction = reaction
self.file = file
self.titleFont = titleFont
//TODO:localize
self.inputFieldNode = PromptInputFieldNode(theme: ptheme, placeholder: "Name", characterLimit: characterLimit)
self.inputFieldNode.text = value ?? ""
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.addSubnode(self.inputFieldNode)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
self.actionNodes.last?.actionEnabled = true
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.inputFieldNode.updateHeight = { [weak self] in
if let strongSelf = self {
if let _ = strongSelf.validLayout {
strongSelf.requestLayout?(.immediate)
}
}
}
self.inputFieldNode.textChanged = { [weak self] text in
if let strongSelf = self, let lastNode = strongSelf.actionNodes.last {
lastNode.actionEnabled = text.count <= characterLimit
strongSelf.requestLayout?(.immediate)
}
}
self.updateTheme(theme)
}
deinit {
self.disposable.dispose()
}
var value: String {
return self.inputFieldNode.text
}
override func updateTheme(_ theme: AlertControllerTheme) {
self.theme = theme
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width, 270.0)
let measureSize = CGSize(width: size.width - 16.0 * 2.0, height: CGFloat.greatestFiniteMagnitude)
let hadValidLayout = self.validLayout != nil
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 16.0)
let spacing: CGFloat = 5.0
let subtextSpacing: CGFloat = -1.0
let textSize = self.textView.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: self.text, font: Font.semibold(17.0), textColor: self.theme.primaryColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0
)),
environment: {},
containerSize: CGSize(width: measureSize.width, height: 1000.0)
)
let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) * 0.5), y: origin.y), size: textSize)
if let textComponentView = self.textView.view {
if textComponentView.superview == nil {
self.view.addSubview(textComponentView)
}
textComponentView.frame = textFrame
}
origin.y += textSize.height + 6.0 + subtextSpacing
let subtextSize = self.subtextView.update(
transition: .immediate,
component: AnyComponent(BalancedTextComponent(
text: .plain(NSAttributedString(string: self.subtext, font: Font.regular(13.0), textColor: self.theme.primaryColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0
)),
environment: {},
containerSize: CGSize(width: measureSize.width, height: 1000.0)
)
let subtextFrame = CGRect(origin: CGPoint(x: floor((size.width - subtextSize.width) * 0.5), y: origin.y), size: subtextSize)
if let subtextComponentView = self.subtextView.view {
if subtextComponentView.superview == nil {
self.view.addSubview(subtextComponentView)
}
subtextComponentView.frame = subtextFrame
}
origin.y += subtextSize.height + 6.0 + spacing
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
var effectiveActionLayout = TextAlertContentActionLayout.horizontal
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 {
effectiveActionLayout = .vertical
}
switch effectiveActionLayout {
case .horizontal:
minActionsWidth += actionTitleSize.width + actionTitleInsets
case .vertical:
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 9.0, right: 18.0)
var contentWidth = max(textSize.width, minActionsWidth)
contentWidth = max(subtextSize.width, minActionsWidth)
contentWidth = max(contentWidth, 234.0)
var actionsHeight: CGFloat = 0.0
switch effectiveActionLayout {
case .horizontal:
actionsHeight = actionButtonHeight
case .vertical:
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
}
let resultWidth = contentWidth + insets.left + insets.right
let inputFieldWidth = resultWidth
let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition)
let inputHeight = inputFieldHeight
let inputFieldFrame = CGRect(x: 0.0, y: origin.y, width: resultWidth, height: inputFieldHeight)
transition.updateFrame(node: self.inputFieldNode, frame: inputFieldFrame)
transition.updateAlpha(node: self.inputFieldNode, alpha: inputHeight > 0.0 ? 1.0 : 0.0)
let emojiSize = CGSize(width: 20.0, height: 20.0)
var visibleEmojiSize = emojiSize
if case .builtin = self.reaction {
visibleEmojiSize = CGSize(width: visibleEmojiSize.width * 2.0, height: visibleEmojiSize.height * 2.0)
}
let _ = self.iconView.update(
transition: .immediate,
component: AnyComponent(EmojiStatusComponent(
context: self.context,
animationCache: self.context.animationCache,
animationRenderer: self.context.animationRenderer,
content: .animation(
content: .file(file: self.file),
size: visibleEmojiSize,
placeholderColor: self.theme.primaryColor.withMultipliedAlpha(0.2),
themeColor: self.theme.primaryColor,
loopMode: .forever
),
isVisibleForAnimations: false,
useSharedAnimation: true,
action: nil,
emojiFileUpdated: nil
)),
environment: {},
containerSize: visibleEmojiSize
)
if let iconComponentView = self.iconView.view {
if iconComponentView.superview == nil {
self.view.addSubview(iconComponentView)
}
let emojiFrame = CGRect(origin: CGPoint(x: inputFieldFrame.minX + 26.0, y: inputFieldFrame.minY + 14.0), size: emojiSize)
iconComponentView.frame = visibleEmojiSize.centered(around: emojiFrame.center)
}
let resultSize = CGSize(width: resultWidth, height: textSize.height + subtextSpacing + subtextSize.height + spacing + inputHeight + actionsHeight + insets.top + insets.bottom)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
switch effectiveActionLayout {
case .horizontal:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
case .vertical:
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
switch effectiveActionLayout {
case .horizontal:
if nodeIndex == self.actionNodes.count - 1 {
currentActionWidth = resultSize.width - actionOffset
} else {
currentActionWidth = actionWidth
}
case .vertical:
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
switch effectiveActionLayout {
case .horizontal:
actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += currentActionWidth
case .vertical:
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
if !hadValidLayout {
self.inputFieldNode.activateInput()
}
return resultSize
}
func animateError() {
self.inputFieldNode.layer.addShakeAnimation()
self.hapticFeedback.error()
}
}
public enum PromptControllerTitleFont {
case regular
case bold
}
public func savedTagNameAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, text: String, subtext: String, titleFont: PromptControllerTitleFont = .regular, value: String?, reaction: MessageReaction.Reaction, file: TelegramMediaFile, characterLimit: Int = 1000, apply: @escaping (String?) -> Void) -> AlertController {
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
var dismissImpl: ((Bool) -> Void)?
var applyImpl: (() -> Void)?
let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?(true)
apply(nil)
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Done, action: {
dismissImpl?(true)
applyImpl?()
})]
let contentNode = SavedTagNameAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, text: text, subtext: subtext, reaction: reaction, file: file, titleFont: titleFont, value: value, characterLimit: characterLimit)
contentNode.complete = {
applyImpl?()
}
applyImpl = { [weak contentNode] in
guard let contentNode = contentNode else {
return
}
apply(contentNode.value)
}
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
let presentationDataDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak controller, weak contentNode] presentationData in
controller?.theme = AlertControllerTheme(presentationData: presentationData)
contentNode?.inputFieldNode.updateTheme(presentationData.theme)
})
controller.dismissed = { _ in
presentationDataDisposable.dispose()
}
dismissImpl = { [weak controller] animated in
contentNode.inputFieldNode.deactivateInput()
if animated {
controller?.dismissAnimated()
} else {
controller?.dismiss()
}
}
return controller
}

View File

@ -0,0 +1,22 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "TopMessageReactions",
module_name = "TopMessageReactions",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/TelegramCore",
"//submodules/Postbox",
"//submodules/AccountContext",
"//submodules/ReactionSelectionNode",
],
visibility = [
"//visibility:public",
],
)

View File

@ -5,7 +5,65 @@ import Postbox
import AccountContext
import ReactionSelectionNode
func tagMessageReactions(context: AccountContext) -> Signal<[ReactionItem], NoError> {
public enum AllowedReactions {
case set(Set<MessageReaction.Reaction>)
case all
}
public func peerMessageAllowedReactions(context: AccountContext, message: Message) -> Signal<AllowedReactions?, NoError> {
if message.id.peerId == context.account.peerId {
return .single(.all)
}
if message.containsSecretMedia {
return .single(AllowedReactions.set(Set()))
}
return combineLatest(
context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: message.id.peerId),
TelegramEngine.EngineData.Item.Peer.AllowedReactions(id: message.id.peerId)
),
context.engine.stickers.availableReactions() |> take(1)
)
|> map { data, availableReactions -> AllowedReactions? in
let (peer, allowedReactions) = data
if let effectiveReactions = message.effectiveReactions(isTags: message.areReactionsTags(accountPeerId: context.account.peerId)), effectiveReactions.count >= 11 {
return .set(Set(effectiveReactions.map(\.value)))
}
switch allowedReactions {
case .unknown:
if case let .channel(channel) = peer, case .broadcast = channel.info {
if let availableReactions = availableReactions {
return .set(Set(availableReactions.reactions.map(\.value)))
} else {
return .set(Set())
}
}
return .all
case let .known(value):
switch value {
case .all:
if case let .channel(channel) = peer, case .broadcast = channel.info {
if let availableReactions = availableReactions {
return .set(Set(availableReactions.reactions.map(\.value)))
} else {
return .set(Set())
}
}
return .all
case let .limited(reactions):
return .set(Set(reactions))
case .empty:
return .set(Set())
}
}
}
}
public func tagMessageReactions(context: AccountContext) -> Signal<[ReactionItem], NoError> {
return combineLatest(
context.engine.stickers.availableReactions(),
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudDefaultTagReactions], namespaces: [ItemCollectionId.Namespace.max - 1], aroundIndex: nil, count: 10000000)
@ -79,7 +137,7 @@ func tagMessageReactions(context: AccountContext) -> Signal<[ReactionItem], NoEr
}
}
func topMessageReactions(context: AccountContext, message: Message) -> Signal<[ReactionItem], NoError> {
public func topMessageReactions(context: AccountContext, message: Message) -> Signal<[ReactionItem], NoError> {
if message.id.peerId == context.account.peerId {
var loadTags = false
if let effectiveReactionsAttribute = message.effectiveReactionsAttribute(isTags: message.areReactionsTags(accountPeerId: context.account.peerId)) {

View File

@ -144,7 +144,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
public let openPeer: (EnginePeer, ChatControllerInteractionNavigateToPeer, MessageReference?, OpenPeerSource) -> Void
public let openPeerMention: (String, Promise<Bool>?) -> Void
public let openMessageContextMenu: (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?, CGPoint?) -> Void
public let updateMessageReaction: (Message, ChatControllerInteractionReaction) -> Void
public let updateMessageReaction: (Message, ChatControllerInteractionReaction, Bool) -> Void
public let openMessageReactionContextMenu: (Message, ContextExtractedContentContainingView, ContextGesture?, MessageReaction.Reaction) -> Void
public let activateMessagePinch: (PinchSourceContainerNode) -> Void
public let openMessageContextActions: (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void
@ -267,7 +267,7 @@ public final class ChatControllerInteraction: ChatControllerInteractionProtocol
openPeerMention: @escaping (String, Promise<Bool>?) -> Void,
openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?, CGPoint?) -> Void,
openMessageReactionContextMenu: @escaping (Message, ContextExtractedContentContainingView, ContextGesture?, MessageReaction.Reaction) -> Void,
updateMessageReaction: @escaping (Message, ChatControllerInteractionReaction) -> Void,
updateMessageReaction: @escaping (Message, ChatControllerInteractionReaction, Bool) -> Void,
activateMessagePinch: @escaping (PinchSourceContainerNode) -> Void,
openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void,
navigateToMessage: @escaping (MessageId, MessageId, NavigateToMessageParams) -> Void,

View File

@ -2531,7 +2531,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
let gesture: ContextGesture? = anyRecognizer as? ContextGesture
let _ = (strongSelf.context.sharedContext.chatAvailableMessageActions(engine: strongSelf.context.engine, accountPeerId: strongSelf.context.account.peerId, messageIds: [message.id])
let _ = (strongSelf.context.sharedContext.chatAvailableMessageActions(engine: strongSelf.context.engine, accountPeerId: strongSelf.context.account.peerId, messageIds: [message.id], keepUpdated: false)
|> deliverOnMainQueue).startStandalone(next: { actions in
guard let strongSelf = self else {
return
@ -2680,7 +2680,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
strongSelf.controller?.window?.presentInGlobalOverlay(controller)
})
}, openMessageReactionContextMenu: { _, _, _, _ in
}, updateMessageReaction: { _, _ in
}, updateMessageReaction: { _, _, _ in
}, activateMessagePinch: { _ in
}, openMessageContextActions: { [weak self] message, node, rect, gesture in
guard let strongSelf = self else {
@ -2697,7 +2697,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
if let previewData = previewData {
let context = strongSelf.context
let strings = strongSelf.presentationData.strings
let items = strongSelf.context.sharedContext.chatAvailableMessageActions(engine: strongSelf.context.engine, accountPeerId: strongSelf.context.account.peerId, messageIds: [message.id])
let items = strongSelf.context.sharedContext.chatAvailableMessageActions(engine: strongSelf.context.engine, accountPeerId: strongSelf.context.account.peerId, messageIds: [message.id], keepUpdated: false)
|> map { actions -> [ContextMenuItem] in
var items: [ContextMenuItem] = []
@ -8996,7 +8996,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
private func deleteMessages(messageIds: Set<MessageId>?) {
if let messageIds = messageIds ?? self.state.selectedMessageIds, !messageIds.isEmpty {
self.activeActionDisposable.set((self.context.sharedContext.chatAvailableMessageActions(engine: self.context.engine, accountPeerId: self.context.account.peerId, messageIds: messageIds)
self.activeActionDisposable.set((self.context.sharedContext.chatAvailableMessageActions(engine: self.context.engine, accountPeerId: self.context.account.peerId, messageIds: messageIds, keepUpdated: false)
|> deliverOnMainQueue).startStrict(next: { [weak self] actions in
if let strongSelf = self, let peer = strongSelf.data?.peer, !actions.options.isEmpty {
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)

View File

@ -94,7 +94,8 @@ swift_library(
"//submodules/Components/BalancedTextComponent",
"//submodules/AnimatedCountLabelNode",
"//submodules/StickerResources",
"//submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent"
"//submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent",
"//submodules/TelegramUI/Components/Stories/StoryQualityUpgradeSheetScreen",
],
visibility = [
"//visibility:public",

View File

@ -52,6 +52,22 @@ public final class StoryContentContextImpl: StoryContentContext {
context.engine.account.viewTracker.refreshCanSendMessagesForPeerIds(peerIds: [peerId])
let preferHighQualityStories: Signal<Bool, NoError> = combineLatest(
context.sharedContext.automaticMediaDownloadSettings
|> map { settings in
return settings.highQualityStories
}
|> distinctUntilChanged,
context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)
)
)
|> map { setting, peer -> Bool in
let isPremium = peer?.isPremium ?? false
return setting && isPremium
}
|> distinctUntilChanged
var inputKeys: [PostboxViewKey] = [
PostboxViewKey.basicPeer(peerId),
PostboxViewKey.cachedPeerData(peerId: peerId),
@ -68,10 +84,11 @@ public final class StoryContentContextImpl: StoryContentContext {
context.engine.data.subscribe(
TelegramEngine.EngineData.Item.NotificationSettings.Global(),
TelegramEngine.EngineData.Item.Peer.IsPremiumRequiredForMessaging(id: peerId)
)
),
preferHighQualityStories
)
|> mapToSignal { _, views, data -> Signal<(CombinedView, [PeerId: Peer], (EngineGlobalNotificationSettings, Bool), [MediaId: TelegramMediaFile], [Int64: EngineStoryItem.ForwardInfo], [StoryId: EngineStoryItem?]), NoError> in
return context.account.postbox.transaction { transaction -> (CombinedView, [PeerId: Peer], (EngineGlobalNotificationSettings, Bool), [MediaId: TelegramMediaFile], [Int64: EngineStoryItem.ForwardInfo], [StoryId: EngineStoryItem?]) in
|> mapToSignal { _, views, data, preferHighQualityStories -> Signal<(CombinedView, [PeerId: Peer], (EngineGlobalNotificationSettings, Bool), [MediaId: TelegramMediaFile], [Int64: EngineStoryItem.ForwardInfo], [StoryId: EngineStoryItem?], Bool), NoError> in
return context.account.postbox.transaction { transaction -> (CombinedView, [PeerId: Peer], (EngineGlobalNotificationSettings, Bool), [MediaId: TelegramMediaFile], [Int64: EngineStoryItem.ForwardInfo], [StoryId: EngineStoryItem?], Bool) in
var peers: [PeerId: Peer] = [:]
var forwardInfoStories: [StoryId: EngineStoryItem?] = [:]
var allEntityFiles: [MediaId: TelegramMediaFile] = [:]
@ -136,10 +153,10 @@ public final class StoryContentContextImpl: StoryContentContext {
}
}
return (views, peers, data, allEntityFiles, pendingForwardsInfo, forwardInfoStories)
return (views, peers, data, allEntityFiles, pendingForwardsInfo, forwardInfoStories, preferHighQualityStories)
}
}
|> deliverOnMainQueue).startStrict(next: { [weak self] views, peers, data, allEntityFiles, pendingForwardsInfo, forwardInfoStories in
|> deliverOnMainQueue).startStrict(next: { [weak self] views, peers, data, allEntityFiles, pendingForwardsInfo, forwardInfoStories, preferHighQualityStories in
guard let self else {
return
}
@ -193,7 +210,8 @@ public final class StoryContentContextImpl: StoryContentContext {
areVoiceMessagesAvailable: cachedUserData.voiceMessagesAvailable,
presence: peerPresence.flatMap { EnginePeer.Presence($0) },
canViewStats: false,
isPremiumRequiredForMessaging: isPremiumRequiredForMessaging
isPremiumRequiredForMessaging: isPremiumRequiredForMessaging,
preferHighQualityStories: preferHighQualityStories
)
} else if let cachedChannelData = cachedPeerDataView.cachedPeerData as? CachedChannelData {
additionalPeerData = StoryContentContextState.AdditionalPeerData(
@ -201,7 +219,8 @@ public final class StoryContentContextImpl: StoryContentContext {
areVoiceMessagesAvailable: true,
presence: peerPresence.flatMap { EnginePeer.Presence($0) },
canViewStats: cachedChannelData.flags.contains(.canViewStats),
isPremiumRequiredForMessaging: isPremiumRequiredForMessaging
isPremiumRequiredForMessaging: isPremiumRequiredForMessaging,
preferHighQualityStories: preferHighQualityStories
)
} else {
additionalPeerData = StoryContentContextState.AdditionalPeerData(
@ -209,7 +228,8 @@ public final class StoryContentContextImpl: StoryContentContext {
areVoiceMessagesAvailable: true,
presence: peerPresence.flatMap { EnginePeer.Presence($0) },
canViewStats: false,
isPremiumRequiredForMessaging: isPremiumRequiredForMessaging
isPremiumRequiredForMessaging: isPremiumRequiredForMessaging,
preferHighQualityStories: preferHighQualityStories
)
}
}
@ -219,7 +239,8 @@ public final class StoryContentContextImpl: StoryContentContext {
areVoiceMessagesAvailable: true,
presence: peerPresence.flatMap { EnginePeer.Presence($0) },
canViewStats: false,
isPremiumRequiredForMessaging: isPremiumRequiredForMessaging
isPremiumRequiredForMessaging: isPremiumRequiredForMessaging,
preferHighQualityStories: preferHighQualityStories
)
}
let state = stateView.value?.get(Stories.PeerState.self)
@ -953,11 +974,18 @@ public final class StoryContentContextImpl: StoryContentContext {
}
}
}
var selectedMedia: EngineMedia
if let slice = stateValue.slice, let alternativeMedia = item.alternativeMedia, !slice.additionalPeerData.preferHighQualityStories {
selectedMedia = alternativeMedia
} else {
selectedMedia = item.media
}
resultResources[mediaId] = StoryPreloadInfo(
peer: peerReference,
storyId: item.id,
media: item.media,
alternativeMedia: item.alternativeMedia,
media: selectedMedia,
reactions: reactions,
priority: .top(position: nextPriority)
)
@ -1106,6 +1134,22 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
context.engine.account.viewTracker.refreshCanSendMessagesForPeerIds(peerIds: [storyId.peerId])
let preferHighQualityStories: Signal<Bool, NoError> = combineLatest(
context.sharedContext.automaticMediaDownloadSettings
|> map { settings in
return settings.highQualityStories
}
|> distinctUntilChanged,
context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)
)
)
|> map { setting, peer -> Bool in
let isPremium = peer?.isPremium ?? false
return setting && isPremium
}
|> distinctUntilChanged
self.storyDisposable = (combineLatest(queue: .mainQueue(),
context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: storyId.peerId),
@ -1172,9 +1216,10 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
}
return (item, peers, allEntityFiles, stories)
}
}
},
preferHighQualityStories
)
|> deliverOnMainQueue).startStrict(next: { [weak self] data, itemAndPeers in
|> deliverOnMainQueue).startStrict(next: { [weak self] data, itemAndPeers, preferHighQualityStories in
guard let self else {
return
}
@ -1193,7 +1238,8 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
areVoiceMessagesAvailable: areVoiceMessagesAvailable,
presence: presence,
canViewStats: canViewStats,
isPremiumRequiredForMessaging: isPremiumRequiredForMessaging
isPremiumRequiredForMessaging: isPremiumRequiredForMessaging,
preferHighQualityStories: preferHighQualityStories
)
for (storyId, story) in forwardInfoStories {
@ -1364,6 +1410,22 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
context.engine.account.viewTracker.refreshCanSendMessagesForPeerIds(peerIds: [peerId])
let preferHighQualityStories: Signal<Bool, NoError> = combineLatest(
context.sharedContext.automaticMediaDownloadSettings
|> map { settings in
return settings.highQualityStories
}
|> distinctUntilChanged,
context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)
)
)
|> map { setting, peer -> Bool in
let isPremium = peer?.isPremium ?? false
return setting && isPremium
}
|> distinctUntilChanged
self.storyDisposable = (combineLatest(queue: .mainQueue(),
context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
@ -1375,9 +1437,10 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
TelegramEngine.EngineData.Item.Peer.IsPremiumRequiredForMessaging(id: peerId)
),
listContext.state,
self.focusedIdUpdated.get()
self.focusedIdUpdated.get(),
preferHighQualityStories
)
|> deliverOnMainQueue).startStrict(next: { [weak self] data, state, _ in
|> deliverOnMainQueue).startStrict(next: { [weak self] data, state, _, preferHighQualityStories in
guard let self else {
return
}
@ -1395,7 +1458,8 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
areVoiceMessagesAvailable: areVoiceMessagesAvailable,
presence: presence,
canViewStats: canViewStats,
isPremiumRequiredForMessaging: isPremiumRequiredForMessaging
isPremiumRequiredForMessaging: isPremiumRequiredForMessaging,
preferHighQualityStories: preferHighQualityStories
)
self.listState = state
@ -1549,11 +1613,17 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
}
}
var selectedMedia: EngineMedia
if let alternativeMedia = item.alternativeMedia, !preferHighQualityStories {
selectedMedia = alternativeMedia
} else {
selectedMedia = item.media
}
resultResources[mediaId] = StoryPreloadInfo(
peer: peerReference,
storyId: item.id,
media: item.media,
alternativeMedia: item.alternativeMedia,
media: selectedMedia,
reactions: reactions,
priority: .top(position: nextPriority)
)
@ -1656,12 +1726,7 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
public func preloadStoryMedia(context: AccountContext, info: StoryPreloadInfo) -> Signal<Never, NoError> {
var signals: [Signal<Never, NoError>] = []
let selectedMedia: EngineMedia
if context.sharedContext.immediateExperimentalUISettings.alternativeStoryMedia, let alternativeMedia = info.alternativeMedia {
selectedMedia = alternativeMedia
} else {
selectedMedia = info.media
}
let selectedMedia: EngineMedia = info.media
switch selectedMedia {
case let .image(image):
@ -1813,10 +1878,30 @@ public func preloadStoryMedia(context: AccountContext, info: StoryPreloadInfo) -
}
public func waitUntilStoryMediaPreloaded(context: AccountContext, peerId: EnginePeer.Id, storyItem: EngineStoryItem) -> Signal<Never, NoError> {
return context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
let preferHighQualityStories: Signal<Bool, NoError> = combineLatest(
context.sharedContext.automaticMediaDownloadSettings
|> map { settings in
return settings.highQualityStories
}
|> distinctUntilChanged,
context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)
)
)
|> mapToSignal { peerValue -> Signal<Never, NoError> in
|> map { setting, peer -> Bool in
let isPremium = peer?.isPremium ?? false
return setting && isPremium
}
|> distinctUntilChanged
return combineLatest(
context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
),
preferHighQualityStories
|> take(1)
)
|> mapToSignal { peerValue, preferHighQualityStories -> Signal<Never, NoError> in
guard let peerValue else {
return .complete()
}
@ -1828,8 +1913,15 @@ public func waitUntilStoryMediaPreloaded(context: AccountContext, peerId: Engine
var loadSignals: [Signal<Never, NoError>] = []
var fetchPriorityDisposable: Disposable?
let selectedMedia: EngineMedia
if !preferHighQualityStories, let alternativeMedia = storyItem.alternativeMedia {
selectedMedia = alternativeMedia
} else {
selectedMedia = storyItem.media
}
var fetchPriorityResourceId: String?
switch storyItem.media {
switch selectedMedia {
case let .image(image):
if let representation = largestImageRepresentation(image.representations) {
fetchPriorityResourceId = representation.resource.id.stringRepresentation
@ -1844,7 +1936,7 @@ public func waitUntilStoryMediaPreloaded(context: AccountContext, peerId: Engine
fetchPriorityDisposable = context.engine.resources.pushPriorityDownload(resourceId: fetchPriorityResourceId, priority: 2)
}
switch storyItem.media {
switch selectedMedia {
case let .image(image):
if let representation = largestImageRepresentation(image.representations) {
statusSignals.append(
@ -1856,7 +1948,7 @@ public func waitUntilStoryMediaPreloaded(context: AccountContext, peerId: Engine
|> ignoreValues
)
loadSignals.append(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(peer.id), userContentType: .story, reference: .media(media: .story(peer: peer, id: storyItem.id, media: storyItem.media._asMedia()), resource: representation.resource), range: nil)
loadSignals.append(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(peer.id), userContentType: .story, reference: .media(media: .story(peer: peer, id: storyItem.id, media: selectedMedia._asMedia()), resource: representation.resource), range: nil)
|> ignoreValues
|> `catch` { _ -> Signal<Never, NoError> in
return .complete()
@ -1886,7 +1978,7 @@ public func waitUntilStoryMediaPreloaded(context: AccountContext, peerId: Engine
|> ignoreValues
)
loadSignals.append(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(peer.id), userContentType: .story, reference: .media(media: .story(peer: peer, id: storyItem.id, media: storyItem.media._asMedia()), resource: file.resource), range: fetchRange)
loadSignals.append(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(peer.id), userContentType: .story, reference: .media(media: .story(peer: peer, id: storyItem.id, media: selectedMedia._asMedia()), resource: file.resource), range: fetchRange)
|> ignoreValues
|> `catch` { _ -> Signal<Never, NoError> in
return .complete()
@ -2144,6 +2236,22 @@ public final class RepostStoriesContentContextImpl: StoryContentContext {
context.engine.account.viewTracker.refreshCanSendMessagesForPeerIds(peerIds: [peerId])
let preferHighQualityStories: Signal<Bool, NoError> = combineLatest(
context.sharedContext.automaticMediaDownloadSettings
|> map { settings in
return settings.highQualityStories
}
|> distinctUntilChanged,
context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)
)
)
|> map { setting, peer -> Bool in
let isPremium = peer?.isPremium ?? false
return setting && isPremium
}
|> distinctUntilChanged
let originalStoryId = StoryId(peerId: originalPeerId, id: originalStory.id)
let inputKeys: [PostboxViewKey] = [
@ -2159,10 +2267,11 @@ public final class RepostStoriesContentContextImpl: StoryContentContext {
context.engine.data.subscribe(
TelegramEngine.EngineData.Item.NotificationSettings.Global(),
TelegramEngine.EngineData.Item.Peer.IsPremiumRequiredForMessaging(id: peerId)
)
),
preferHighQualityStories
)
|> mapToSignal { _, views, data -> Signal<(CombinedView, [PeerId: Peer], (EngineGlobalNotificationSettings, Bool), [MediaId: TelegramMediaFile], [StoryId: EngineStoryItem?]), NoError> in
return context.account.postbox.transaction { transaction -> (CombinedView, [PeerId: Peer], (EngineGlobalNotificationSettings, Bool), [MediaId: TelegramMediaFile], [StoryId: EngineStoryItem?]) in
|> mapToSignal { _, views, data, preferHighQualityStories -> Signal<(CombinedView, [PeerId: Peer], (EngineGlobalNotificationSettings, Bool), [MediaId: TelegramMediaFile], [StoryId: EngineStoryItem?], Bool), NoError> in
return context.account.postbox.transaction { transaction -> (CombinedView, [PeerId: Peer], (EngineGlobalNotificationSettings, Bool), [MediaId: TelegramMediaFile], [StoryId: EngineStoryItem?], Bool) in
var peers: [PeerId: Peer] = [:]
var forwardInfoStories: [StoryId: EngineStoryItem?] = [:]
var allEntityFiles: [MediaId: TelegramMediaFile] = [:]
@ -2204,10 +2313,10 @@ public final class RepostStoriesContentContextImpl: StoryContentContext {
}
}
return (views, peers, data, allEntityFiles, forwardInfoStories)
return (views, peers, data, allEntityFiles, forwardInfoStories, preferHighQualityStories)
}
}
|> deliverOnMainQueue).startStrict(next: { [weak self] views, peers, data, allEntityFiles, forwardInfoStories in
|> deliverOnMainQueue).startStrict(next: { [weak self] views, peers, data, allEntityFiles, forwardInfoStories, preferHighQualityStories in
guard let self else {
return
}
@ -2257,7 +2366,8 @@ public final class RepostStoriesContentContextImpl: StoryContentContext {
areVoiceMessagesAvailable: cachedUserData.voiceMessagesAvailable,
presence: peerPresence.flatMap { EnginePeer.Presence($0) },
canViewStats: false,
isPremiumRequiredForMessaging: isPremiumRequiredForMessaging
isPremiumRequiredForMessaging: isPremiumRequiredForMessaging,
preferHighQualityStories: preferHighQualityStories
)
} else if let cachedChannelData = cachedPeerDataView.cachedPeerData as? CachedChannelData {
additionalPeerData = StoryContentContextState.AdditionalPeerData(
@ -2265,7 +2375,8 @@ public final class RepostStoriesContentContextImpl: StoryContentContext {
areVoiceMessagesAvailable: true,
presence: peerPresence.flatMap { EnginePeer.Presence($0) },
canViewStats: cachedChannelData.flags.contains(.canViewStats),
isPremiumRequiredForMessaging: isPremiumRequiredForMessaging
isPremiumRequiredForMessaging: isPremiumRequiredForMessaging,
preferHighQualityStories: preferHighQualityStories
)
} else {
additionalPeerData = StoryContentContextState.AdditionalPeerData(
@ -2273,7 +2384,8 @@ public final class RepostStoriesContentContextImpl: StoryContentContext {
areVoiceMessagesAvailable: true,
presence: peerPresence.flatMap { EnginePeer.Presence($0) },
canViewStats: false,
isPremiumRequiredForMessaging: isPremiumRequiredForMessaging
isPremiumRequiredForMessaging: isPremiumRequiredForMessaging,
preferHighQualityStories: preferHighQualityStories
)
}
}
@ -2283,7 +2395,8 @@ public final class RepostStoriesContentContextImpl: StoryContentContext {
areVoiceMessagesAvailable: true,
presence: peerPresence.flatMap { EnginePeer.Presence($0) },
canViewStats: false,
isPremiumRequiredForMessaging: isPremiumRequiredForMessaging
isPremiumRequiredForMessaging: isPremiumRequiredForMessaging,
preferHighQualityStories: preferHighQualityStories
)
}
@ -2720,11 +2833,18 @@ public final class RepostStoriesContentContextImpl: StoryContentContext {
}
}
}
var selectedMedia: EngineMedia
if let slice = stateValue.slice, let alternativeMedia = item.alternativeMedia, !slice.additionalPeerData.preferHighQualityStories {
selectedMedia = alternativeMedia
} else {
selectedMedia = item.media
}
resultResources[mediaId] = StoryPreloadInfo(
peer: peerReference,
storyId: item.id,
media: item.media,
alternativeMedia: item.alternativeMedia,
media: selectedMedia,
reactions: reactions,
priority: .top(position: nextPriority)
)

View File

@ -142,19 +142,22 @@ public final class StoryContentContextState {
public let presence: EnginePeer.Presence?
public let canViewStats: Bool
public let isPremiumRequiredForMessaging: Bool
public let preferHighQualityStories: Bool
public init(
isMuted: Bool,
areVoiceMessagesAvailable: Bool,
presence: EnginePeer.Presence?,
canViewStats: Bool,
isPremiumRequiredForMessaging: Bool
isPremiumRequiredForMessaging: Bool,
preferHighQualityStories: Bool
) {
self.isMuted = isMuted
self.areVoiceMessagesAvailable = areVoiceMessagesAvailable
self.presence = presence
self.canViewStats = canViewStats
self.isPremiumRequiredForMessaging = isPremiumRequiredForMessaging
self.preferHighQualityStories = preferHighQualityStories
}
public static func == (lhs: StoryContentContextState.AdditionalPeerData, rhs: StoryContentContextState.AdditionalPeerData) -> Bool {
@ -173,6 +176,9 @@ public final class StoryContentContextState {
if lhs.isPremiumRequiredForMessaging != rhs.isPremiumRequiredForMessaging {
return false
}
if lhs.preferHighQualityStories != rhs.preferHighQualityStories {
return false
}
return true
}
}

View File

@ -35,9 +35,10 @@ final class StoryItemContentComponent: Component {
let audioMode: StoryContentItem.AudioMode
let isVideoBuffering: Bool
let isCurrent: Bool
let preferHighQuality: Bool
let activateReaction: (UIView, MessageReaction.Reaction) -> Void
init(context: AccountContext, strings: PresentationStrings, peer: EnginePeer, item: EngineStoryItem, availableReactions: StoryAvailableReactions?, entityFiles: [MediaId: TelegramMediaFile], audioMode: StoryContentItem.AudioMode, isVideoBuffering: Bool, isCurrent: Bool, activateReaction: @escaping (UIView, MessageReaction.Reaction) -> Void) {
init(context: AccountContext, strings: PresentationStrings, peer: EnginePeer, item: EngineStoryItem, availableReactions: StoryAvailableReactions?, entityFiles: [MediaId: TelegramMediaFile], audioMode: StoryContentItem.AudioMode, isVideoBuffering: Bool, isCurrent: Bool, preferHighQuality: Bool, activateReaction: @escaping (UIView, MessageReaction.Reaction) -> Void) {
self.context = context
self.strings = strings
self.peer = peer
@ -47,6 +48,7 @@ final class StoryItemContentComponent: Component {
self.audioMode = audioMode
self.isVideoBuffering = isVideoBuffering
self.isCurrent = isCurrent
self.preferHighQuality = preferHighQuality
self.activateReaction = activateReaction
}
@ -74,6 +76,9 @@ final class StoryItemContentComponent: Component {
}
if lhs.isCurrent != rhs.isCurrent {
return false
}
if lhs.preferHighQuality != rhs.preferHighQuality {
return false
}
return true
}
@ -576,7 +581,7 @@ final class StoryItemContentComponent: Component {
let selectedMedia: EngineMedia
var messageMedia: EngineMedia?
if component.context.sharedContext.immediateExperimentalUISettings.alternativeStoryMedia, let alternativeMedia = component.item.alternativeMedia {
if !component.preferHighQuality, let alternativeMedia = component.item.alternativeMedia {
selectedMedia = alternativeMedia
switch alternativeMedia {

View File

@ -1567,6 +1567,7 @@ public final class StoryItemSetContainerComponent: Component {
audioMode: component.audioMode,
isVideoBuffering: visibleItem.isBuffering,
isCurrent: index == centralIndex,
preferHighQuality: component.slice.additionalPeerData.preferHighQualityStories,
activateReaction: { [weak self] reactionView, reaction in
guard let self else {
return
@ -4379,6 +4380,7 @@ public final class StoryItemSetContainerComponent: Component {
items: reactionItems.map(ReactionContextItem.reaction),
selectedItems: component.slice.item.storyItem.myReaction.flatMap { Set([$0]) } ?? Set(),
title: self.displayLikeReactions ? nil : component.strings.Story_SendReactionAsMessage,
reactionsLocked: false,
alwaysAllowPremiumReactions: false,
allPresetReactionsAreAvailable: false,
getEmojiContent: { [weak self] animationCache, animationRenderer in
@ -5611,6 +5613,16 @@ public final class StoryItemSetContainerComponent: Component {
), nil)
}
private func presentQualityUpgradeScreen() {
self.sendMessageContext.presentQualityUpgrade(view: self, action: { [weak self] in
guard let self else {
return
}
//TODO:localize
self.presentStoriesUpgradeScreen(source: .storiesStealthMode)
})
}
private func presentStealthModeUpgradeScreen() {
self.sendMessageContext.presentStealthModeUpgrade(view: self, action: { [weak self] in
guard let self else {
@ -6536,6 +6548,88 @@ public final class StoryItemSetContainerComponent: Component {
})))
}
if !component.slice.peer.isService && component.slice.item.storyItem.isPublic && (component.slice.peer.addressName != nil || !component.slice.peer._asPeer().usernames.isEmpty) {
items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_CopyLink, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
a(.default)
guard let self, let component = self.component else {
return
}
let _ = (component.context.engine.messages.exportStoryLink(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id)
|> deliverOnMainQueue).startStandalone(next: { [weak self] link in
guard let self, let component = self.component else {
return
}
if let link {
UIPasteboard.general.string = link
component.presentController(UndoOverlayController(
presentationData: presentationData,
content: .linkCopied(text: component.strings.Story_ToastLinkCopied),
elevatedLayout: false,
animateInAsReplacement: false,
blurred: true,
action: { _ in return false }
), nil)
}
})
})))
}
if case let .file(file) = component.slice.item.storyItem.media, file.isVideo {
//TODO:localize
let isHq = component.slice.additionalPeerData.preferHighQualityStories
items.append(.action(ContextMenuActionItem(text: isHq ? "Decrease Quality" : "Increase Quality", icon: { theme in
if isHq {
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/QualitySd"), color: theme.contextMenu.primaryColor)
} else {
return generateTintedImage(image: UIImage(bundleImageName: accountUser.isPremium ? "Chat/Context Menu/QualityHd" : "Chat/Context Menu/QualityHdLocked"), color: theme.contextMenu.primaryColor)
}
}, action: { [weak self] _, a in
a(.default)
guard let self, let component = self.component, let controller = component.controller() else {
return
}
if !component.slice.additionalPeerData.preferHighQualityStories && !accountUser.isPremium {
//TODO:localize
self.presentQualityUpgradeScreen()
return
}
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
//TODO:localize
let title: String
let text: String
if component.slice.additionalPeerData.preferHighQualityStories {
title = "Quality Lowered"
text = "Stories will now download faster."
} else {
title = "Quality Increased"
text = "You can lower the quality later for faster downloads."
}
controller.present(UndoOverlayController(
presentationData: presentationData,
content: .info(title: title, text: text, timeout: nil, customUndoText: nil),
elevatedLayout: false,
animateInAsReplacement: false,
blurred: true,
action: { _ in return false }
), in: .current)
let _ = updateMediaDownloadSettingsInteractively(accountManager: component.context.sharedContext.accountManager, { settings in
var settings = settings
settings.highQualityStories = !isHq
return settings
}).startStandalone()
})))
}
var isHidden = false
if case let .user(user) = component.slice.peer, let storiesHidden = user.storiesHidden {
isHidden = storiesHidden
@ -6633,37 +6727,6 @@ public final class StoryItemSetContainerComponent: Component {
})))
}
if !component.slice.peer.isService && component.slice.item.storyItem.isPublic && (component.slice.peer.addressName != nil || !component.slice.peer._asPeer().usernames.isEmpty) {
items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_CopyLink, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
a(.default)
guard let self, let component = self.component else {
return
}
let _ = (component.context.engine.messages.exportStoryLink(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id)
|> deliverOnMainQueue).startStandalone(next: { [weak self] link in
guard let self, let component = self.component else {
return
}
if let link {
UIPasteboard.general.string = link
component.presentController(UndoOverlayController(
presentationData: presentationData,
content: .linkCopied(text: component.strings.Story_ToastLinkCopied),
elevatedLayout: false,
animateInAsReplacement: false,
blurred: true,
action: { _ in return false }
), nil)
}
})
})))
}
if component.slice.additionalPeerData.canViewStats {
items.append(.action(ContextMenuActionItem(text: component.strings.Story_Context_ViewStats, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Statistics"), color: theme.contextMenu.primaryColor)

View File

@ -49,6 +49,7 @@ import TelegramNotices
import ObjectiveC
import LocationUI
import ReactionSelectionNode
import StoryQualityUpgradeSheetScreen
private var ObjCKey_DeinitWatcher: Int?
@ -3277,6 +3278,29 @@ final class StoryItemSetContainerSendMessage {
controller.push(sheet)
})
}
func presentQualityUpgrade(view: StoryItemSetContainerComponent.View, action: @escaping () -> Void) {
guard let component = view.component, let controller = component.controller() else {
return
}
let sheet = StoryQualityUpgradeSheetScreen(
context: component.context,
buttonAction: {
action()
}
)
sheet.wasDismissed = { [weak self, weak view] in
guard let self, let view else {
return
}
self.actionSheet = nil
view.updateIsProgressPaused()
}
self.actionSheet = sheet
view.updateIsProgressPaused()
controller.push(sheet)
}
private var selectedMediaArea: MediaArea?
func activateMediaArea(view: StoryItemSetContainerComponent.View, mediaArea: MediaArea, position: CGPoint? = nil, immediate: Bool = false) {

View File

@ -0,0 +1,29 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "StoryQualityUpgradeSheetScreen",
module_name = "StoryQualityUpgradeSheetScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/TelegramPresentationData",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/AccountContext",
"//submodules/Components/MultilineTextComponent",
"//submodules/Components/BalancedTextComponent",
"//submodules/Components/BundleIconComponent",
"//submodules/Components/ViewControllerComponent",
"//submodules/Components/SheetComponent",
"//submodules/TelegramUI/Components/ButtonComponent",
"//submodules/TelegramUI/Components/LottieComponent",
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,428 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import ViewControllerComponent
import AccountContext
import SheetComponent
import ButtonComponent
import LottieComponent
import MultilineTextComponent
import BalancedTextComponent
import Markdown
import TelegramStringFormatting
import BundleIconComponent
public final class ButtonSubtitleComponent: CombinedComponent {
public let title: String
public let color: UIColor
public init(title: String, color: UIColor) {
self.title = title
self.color = color
}
public static func ==(lhs: ButtonSubtitleComponent, rhs: ButtonSubtitleComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.color !== rhs.color {
return false
}
return true
}
public static var body: Body {
let icon = Child(BundleIconComponent.self)
let text = Child(Text.self)
return { context in
let icon = icon.update(
component: BundleIconComponent(
name: "Chat/Input/Accessory Panels/TextLockIcon",
tintColor: context.component.color,
maxSize: CGSize(width: 10.0, height: 10.0)
),
availableSize: CGSize(width: 100.0, height: 100.0),
transition: context.transition
)
let text = text.update(
component: Text(text: context.component.title, font: Font.medium(11.0), color: context.component.color),
availableSize: CGSize(width: context.availableSize.width - 20.0, height: 100.0),
transition: context.transition
)
let spacing: CGFloat = 3.0
let size = CGSize(width: icon.size.width + spacing + text.size.width, height: text.size.height)
context.add(icon
.position(icon.size.centered(in: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: icon.size.width, height: size.height))).center)
)
context.add(text
.position(text.size.centered(in: CGRect(origin: CGPoint(x: icon.size.width + spacing, y: 0.0), size: text.size)).center)
)
return size
}
}
}
private final class StoryQualityUpgradeSheetContentComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let action: () -> Void
let dismiss: () -> Void
init(
action: @escaping () -> Void,
dismiss: @escaping () -> Void
) {
self.action = action
self.dismiss = dismiss
}
static func ==(lhs: StoryQualityUpgradeSheetContentComponent, rhs: StoryQualityUpgradeSheetContentComponent) -> Bool {
return true
}
final class View: UIView {
private let icon = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private let text = ComponentView<Empty>()
private let button = ComponentView<Empty>()
private var cancelButton: ComponentView<Empty>?
private var component: StoryQualityUpgradeSheetContentComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
func update(component: StoryQualityUpgradeSheetContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
self.component = component
self.state = state
let environment = environment[EnvironmentType.self].value
let sideInset: CGFloat = 16.0
let cancelButton: ComponentView<Empty>
if let current = self.cancelButton {
cancelButton = current
} else {
cancelButton = ComponentView()
self.cancelButton = cancelButton
}
let cancelButtonSize = cancelButton.update(
transition: transition,
component: AnyComponent(Button(
content: AnyComponent(Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: environment.theme.list.itemAccentColor)),
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.dismiss()
}
).minSize(CGSize(width: 8.0, height: 44.0))),
environment: {},
containerSize: CGSize(width: 200.0, height: 100.0)
)
if let cancelButtonView = cancelButton.view {
if cancelButtonView.superview == nil {
self.addSubview(cancelButtonView)
}
transition.setFrame(view: cancelButtonView, frame: CGRect(origin: CGPoint(x: 16.0, y: 6.0), size: cancelButtonSize))
}
var contentHeight: CGFloat = 0.0
contentHeight += 32.0
let iconSize = self.icon.update(
transition: transition,
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "ChatListNoResults"),
color: nil,
startingPosition: .begin,
size: CGSize(width: 120.0, height: 120.0)
)),
environment: {},
containerSize: CGSize(width: 120.0, height: 120.0)
)
if let iconView = self.icon.view {
if iconView.superview == nil {
self.addSubview(iconView)
}
transition.setFrame(view: iconView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: 25.0), size: iconSize))
}
contentHeight += 138.0
//TODO:localize
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "High-Quality Stories", font: Font.semibold(20.0), textColor: environment.theme.list.itemPrimaryTextColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentHeight), size: titleSize))
}
contentHeight += titleSize.height
contentHeight += 14.0
//TODO:localize
let textSize = self.text.update(
transition: transition,
component: AnyComponent(BalancedTextComponent(
text: .plain(NSAttributedString(string: "Subscribe to premium to view stories in higher resolution.", font: Font.regular(14.0), textColor: environment.theme.list.itemSecondaryTextColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.18
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0)
)
if let textView = self.text.view {
if textView.superview == nil {
self.addSubview(textView)
}
transition.setFrame(view: textView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - textSize.width) * 0.5), y: contentHeight), size: textSize))
}
contentHeight += textSize.height
contentHeight += 12.0
contentHeight += 32.0
//TODO:localize
var buttonContents: [AnyComponentWithIdentity<Empty>] = []
buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(
Text(text: "Increase Quality", font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor)
)))
buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(ButtonSubtitleComponent(
title: "Premium Required",
color: environment.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.7)
))))
let buttonSize = self.button.update(
transition: transition,
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
color: environment.theme.list.itemCheckColors.fillColor,
foreground: environment.theme.list.itemCheckColors.foregroundColor,
pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8)
),
content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(
VStack(buttonContents, spacing: 3.0)
)),
isEnabled: true,
allowActionWhenDisabled: true,
displaysProgress: false,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.action()
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0)
)
let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: buttonSize)
if let buttonView = self.button.view {
if buttonView.superview == nil {
self.addSubview(buttonView)
}
transition.setFrame(view: buttonView, frame: buttonFrame)
}
contentHeight += buttonSize.height
if environment.safeInsets.bottom.isZero {
contentHeight += 16.0
} else {
contentHeight += environment.safeInsets.bottom + 14.0
}
return CGSize(width: availableSize.width, height: contentHeight)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private final class StoryQualityUpgradeSheetScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let buttonAction: (() -> Void)?
init(
context: AccountContext,
buttonAction: (() -> Void)?
) {
self.context = context
self.buttonAction = buttonAction
}
static func ==(lhs: StoryQualityUpgradeSheetScreenComponent, rhs: StoryQualityUpgradeSheetScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
return true
}
final class View: UIView {
private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, SheetComponentEnvironment)>()
private let sheetAnimateOut = ActionSlot<Action<Void>>()
private var component: StoryQualityUpgradeSheetScreenComponent?
private var environment: EnvironmentType?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: StoryQualityUpgradeSheetScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
self.component = component
let environment = environment[ViewControllerComponentContainer.Environment.self].value
self.environment = environment
let sheetEnvironment = SheetComponentEnvironment(
isDisplaying: environment.isVisible,
isCentered: environment.metrics.widthClass == .regular,
hasInputHeight: !environment.inputHeight.isZero,
regularMetricsSize: CGSize(width: 430.0, height: 900.0),
dismiss: { [weak self] _ in
guard let self, let environment = self.environment else {
return
}
self.sheetAnimateOut.invoke(Action { _ in
if let controller = environment.controller() {
controller.dismiss(completion: nil)
}
})
}
)
let _ = self.sheet.update(
transition: transition,
component: AnyComponent(SheetComponent(
content: AnyComponent(StoryQualityUpgradeSheetContentComponent(
action: { [weak self] in
guard let self else {
return
}
self.sheetAnimateOut.invoke(Action { [weak self] _ in
if let controller = environment.controller() {
controller.dismiss(completion: nil)
}
guard let self else {
return
}
self.component?.buttonAction?()
})
},
dismiss: {
self.sheetAnimateOut.invoke(Action { _ in
if let controller = environment.controller() {
controller.dismiss(completion: nil)
}
})
}
)),
backgroundColor: .color(environment.theme.overallDarkAppearance ? environment.theme.list.itemBlocksBackgroundColor : environment.theme.list.blocksBackgroundColor),
animateOut: self.sheetAnimateOut
)),
environment: {
environment
sheetEnvironment
},
containerSize: availableSize
)
if let sheetView = self.sheet.view {
if sheetView.superview == nil {
self.addSubview(sheetView)
}
transition.setFrame(view: sheetView, frame: CGRect(origin: CGPoint(), size: availableSize))
}
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public class StoryQualityUpgradeSheetScreen: ViewControllerComponentContainer {
public init(
context: AccountContext,
buttonAction: (() -> Void)? = nil
) {
super.init(context: context, component: StoryQualityUpgradeSheetScreenComponent(
context: context,
buttonAction: buttonAction
), navigationBarAppearance: .none, theme: .dark)
self.statusBar.statusBarStyle = .Ignore
self.navigationPresentation = .flatModal
self.blocksBackgroundWhenInOverlay = true
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.view.disablesInteractiveModalDismiss = true
}
override public func dismiss(completion: (() -> Void)? = nil) {
super.dismiss(completion: {
completion?()
})
self.wasDismissed?()
}
}

View File

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "IconDownloadLocked.svg",
"filename" : "downloadlocked_24.pdf",
"idiom" : "universal"
}
],

View File

@ -1,7 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 19.748C13.3608 19.9125 12.6906 20 12 20C7.58172 20 4 16.4183 4 12C4 7.58172 7.58172 4 12 4C16.0796 4 19.446 7.05369 19.9381 11" stroke="white" stroke-width="1.33" stroke-linecap="round"/>
<path d="M12 14V8.5" stroke="white" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 12.5L12 15.5L15 12.5" stroke="white" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20 12.835C18.252 12.835 16.835 14.252 16.835 16V17.5307C16.3253 17.9588 16 18.6076 16 19.3333V21.6667C16 22.9544 17.0242 24 18.2857 24H21.7143C22.9758 24 24 22.9544 24 21.6667V19.3333C24 18.6076 23.6747 17.9588 23.165 17.5307V16C23.165 14.252 21.748 12.835 20 12.835ZM21.835 17.0032V16C21.835 14.9866 21.0134 14.165 20 14.165C18.9866 14.165 18.165 14.9866 18.165 16V17.0032C18.205 17.0011 18.2452 17 18.2857 17H21.7143C21.7548 17 21.795 17.0011 21.835 17.0032Z" fill="white" style="mix-blend-mode:overlay"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20 12.835C18.252 12.835 16.835 14.252 16.835 16V17.5307C16.3253 17.9588 16 18.6076 16 19.3333V21.6667C16 22.9544 17.0242 24 18.2857 24H21.7143C22.9758 24 24 22.9544 24 21.6667V19.3333C24 18.6076 23.6747 17.9588 23.165 17.5307V16C23.165 14.252 21.748 12.835 20 12.835ZM21.835 17.0032V16C21.835 14.9866 21.0134 14.165 20 14.165C18.9866 14.165 18.165 14.9866 18.165 16V17.0032C18.205 17.0011 18.2452 17 18.2857 17H21.7143C21.7548 17 21.795 17.0011 21.835 17.0032Z" fill="white" fill-opacity="0.4" style="mix-blend-mode:overlay"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,214 @@
%PDF-1.7
1 0 obj
<< /Type /XObject
/Length 2 0 R
/Group << /Type /Group
/S /Transparency
/I true
>>
/Subtype /Form
/Resources << /ExtGState << /E1 << /ca 0.400000 >> >> >>
/BBox [ 0.000000 0.000000 24.000000 24.000000 ]
>>
stream
q
/E1 gs
1.000000 0.000000 -0.000000 1.000000 16.000000 0.000000 cm
0.000000 0.000000 0.000000 scn
4.000000 11.165039 m
2.252019 11.165039 0.835000 9.748020 0.835000 8.000039 c
0.835000 6.469324 l
0.325283 6.041227 0.000000 5.392427 0.000000 4.666706 c
0.000000 2.333372 l
0.000000 1.045606 1.024229 0.000039 2.285714 0.000039 c
5.714286 0.000039 l
6.975772 0.000039 8.000000 1.045606 8.000000 2.333372 c
8.000000 4.666706 l
8.000000 5.392427 7.674717 6.041228 7.165000 6.469325 c
7.165000 8.000039 l
7.165000 9.748020 5.747981 11.165039 4.000000 11.165039 c
h
5.835000 6.996834 m
5.835000 8.000039 l
5.835000 9.013481 5.013443 9.835039 4.000000 9.835039 c
2.986557 9.835039 2.165000 9.013481 2.165000 8.000039 c
2.165000 6.996834 l
2.204976 6.998962 2.245223 7.000039 2.285714 7.000039 c
5.714286 7.000039 l
5.754777 7.000039 5.795024 6.998962 5.835000 6.996834 c
h
f*
n
Q
endstream
endobj
2 0 obj
874
endobj
3 0 obj
<< /XObject << /X1 1 0 R >>
/ExtGState << /E2 << /ca 1.000000 >>
/E1 << /BM /Overlay >>
>>
>>
endobj
4 0 obj
<< /Length 5 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 4.000000 2.669922 cm
0.000000 0.000000 0.000000 scn
10.165757 0.938103 m
10.521434 1.029648 10.735556 1.392193 10.644011 1.747870 c
10.552465 2.103547 10.189920 2.317669 9.834243 2.226124 c
10.165757 0.938103 l
h
15.278217 10.247791 m
15.323663 9.883345 15.655947 9.624743 16.020393 9.670189 c
16.384840 9.715635 16.643442 10.047918 16.597996 10.412365 c
15.278217 10.247791 l
h
8.000000 1.995078 m
3.948991 1.995078 0.665000 5.279069 0.665000 9.330078 c
-0.665000 9.330078 l
-0.665000 4.544531 3.214453 0.665077 8.000000 0.665077 c
8.000000 1.995078 l
h
0.665000 9.330078 m
0.665000 13.381087 3.948991 16.665077 8.000000 16.665077 c
8.000000 17.995079 l
3.214453 17.995079 -0.665000 14.115625 -0.665000 9.330078 c
0.665000 9.330078 l
h
9.834243 2.226124 m
9.248698 2.075415 8.634215 1.995078 8.000000 1.995078 c
8.000000 0.665077 l
8.746983 0.665077 9.472822 0.759754 10.165757 0.938103 c
9.834243 2.226124 l
h
8.000000 16.665077 m
11.739998 16.665077 14.827109 13.865385 15.278217 10.247791 c
16.597996 10.412365 l
16.064907 14.687395 12.419238 17.995079 8.000000 17.995079 c
8.000000 16.665077 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 12.000000 8.669922 cm
0.000000 0.000000 0.000000 scn
-0.665000 1.330078 m
-0.665000 0.962809 -0.367269 0.665078 0.000000 0.665078 c
0.367269 0.665078 0.665000 0.962809 0.665000 1.330078 c
-0.665000 1.330078 l
h
0.665000 6.830078 m
0.665000 7.197348 0.367269 7.495078 0.000000 7.495078 c
-0.367269 7.495078 -0.665000 7.197348 -0.665000 6.830078 c
0.665000 6.830078 l
h
0.665000 1.330078 m
0.665000 6.830078 l
-0.665000 6.830078 l
-0.665000 1.330078 l
0.665000 1.330078 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 -1.000000 9.000000 12.959961 cm
0.000000 0.000000 0.000000 scn
-0.470226 1.930187 m
-0.729925 1.670488 -0.729925 1.249434 -0.470226 0.989735 c
-0.210527 0.730036 0.210527 0.730036 0.470226 0.989735 c
-0.470226 1.930187 l
h
3.000000 4.459961 m
3.470226 4.930187 l
3.210527 5.189886 2.789473 5.189886 2.529774 4.930187 c
3.000000 4.459961 l
h
5.529774 0.989735 m
5.789473 0.730036 6.210527 0.730036 6.470226 0.989735 c
6.729925 1.249434 6.729925 1.670488 6.470226 1.930187 c
5.529774 0.989735 l
h
0.470226 0.989735 m
3.470226 3.989735 l
2.529774 4.930187 l
-0.470226 1.930187 l
0.470226 0.989735 l
h
2.529774 3.989735 m
5.529774 0.989735 l
6.470226 1.930187 l
3.470226 4.930187 l
2.529774 3.989735 l
h
f
n
Q
q
/E1 gs
/E2 gs
/X1 Do
Q
endstream
endobj
5 0 obj
2452
endobj
6 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 3 0 R
/Contents 4 0 R
/Parent 7 0 R
>>
endobj
7 0 obj
<< /Kids [ 6 0 R ]
/Count 1
/Type /Pages
>>
endobj
8 0 obj
<< /Pages 7 0 R
/Type /Catalog
>>
endobj
xref
0 9
0000000000 65535 f
0000000010 00000 n
0000001195 00000 n
0000001217 00000 n
0000001371 00000 n
0000003879 00000 n
0000003902 00000 n
0000004075 00000 n
0000004149 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 8 0 R
/Size 9
>>
startxref
4208
%%EOF

View File

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "IconEyeLocked.svg",
"filename" : "eyelocked_24.pdf",
"idiom" : "universal"
}
],

View File

@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.47037 3.52973C4.21067 3.27004 3.78962 3.27004 3.52992 3.52973C3.27022 3.78943 3.27022 4.21049 3.52992 4.47019L13.7099 14.6502C13.9246 14.8649 14.2495 14.9021 14.5024 14.7618L14.5046 14.7641C14.508 14.761 14.5114 14.7579 14.5148 14.7548C14.5632 14.7266 14.6089 14.6917 14.6504 14.6502C14.6919 14.6087 14.7267 14.5631 14.755 14.5147C15.3607 13.8515 15.7302 12.9688 15.7302 11.9999C15.7302 9.93983 14.0602 8.26985 12.0002 8.26985C11.2103 8.26985 10.4778 8.51535 9.87483 8.93419L8.67856 7.73793C9.64518 7.32933 10.7528 7.06502 12.0008 7.06502C16.117 7.06502 18.707 9.94002 19.7462 11.3878C19.9603 11.6861 20.3758 11.7544 20.6742 11.5402C20.9726 11.3261 21.0408 10.9106 20.8267 10.6122C19.7141 9.06223 16.7778 5.73502 12.0008 5.73502C10.3346 5.73502 8.89242 6.1397 7.67332 6.73268L4.47037 3.52973ZM10.8394 9.89874L14.1013 13.1607C14.2918 12.8166 14.4002 12.4209 14.4002 11.9999C14.4002 10.6744 13.3257 9.59985 12.0002 9.59985C11.5791 9.59985 11.1834 9.70828 10.8394 9.89874ZM5.77418 7.9145C4.47929 8.91081 3.5894 10.0209 3.10255 10.7141C2.64774 11.3616 2.63304 12.2024 3.05545 12.8663C4.06881 14.4589 7.00176 18.265 12.0008 18.265C12.7635 18.265 13.4808 18.1758 14.1521 18.0181C14.5097 17.9342 14.7314 17.5762 14.6475 17.2187C14.5635 16.8612 14.2056 16.6394 13.848 16.7234C13.2741 16.8582 12.6588 16.935 12.0008 16.935C7.72719 16.935 5.14063 13.6659 4.17757 12.1523C4.04178 11.9389 4.04807 11.6819 4.19091 11.4785C4.66462 10.8041 5.50984 9.7646 6.72349 8.86381L5.77418 7.9145ZM8.51851 10.6588C8.35811 11.075 8.27018 11.5272 8.27018 11.9999C8.27018 14.0599 9.94016 15.7299 12.0002 15.7299C12.4729 15.7299 12.925 15.6419 13.3412 15.4815L12.247 14.3873C12.1659 14.3956 12.0835 14.3999 12.0002 14.3999C10.6747 14.3999 9.60018 13.3253 9.60018 11.9999C9.60018 11.9165 9.60443 11.8342 9.61272 11.753L8.51851 10.6588Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20 12.835C18.252 12.835 16.835 14.252 16.835 16V17.5307C16.3253 17.9588 16 18.6076 16 19.3333V21.6666C16 22.9544 17.0242 24 18.2857 24H21.7143C22.9758 24 24 22.9544 24 21.6666V19.3333C24 18.6076 23.6747 17.9588 23.165 17.5307V16C23.165 14.252 21.748 12.835 20 12.835ZM21.835 17.0032V16C21.835 14.9865 21.0134 14.165 20 14.165C18.9866 14.165 18.165 14.9865 18.165 16V17.0032C18.205 17.001 18.2452 17 18.2857 17H21.7143C21.7548 17 21.795 17.001 21.835 17.0032Z" fill="white" style="mix-blend-mode:overlay"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20 12.835C18.252 12.835 16.835 14.252 16.835 16V17.5307C16.3253 17.9588 16 18.6076 16 19.3333V21.6666C16 22.9544 17.0242 24 18.2857 24H21.7143C22.9758 24 24 22.9544 24 21.6666V19.3333C24 18.6076 23.6747 17.9588 23.165 17.5307V16C23.165 14.252 21.748 12.835 20 12.835ZM21.835 17.0032V16C21.835 14.9865 21.0134 14.165 20 14.165C18.9866 14.165 18.165 14.9865 18.165 16V17.0032C18.205 17.001 18.2452 17 18.2857 17H21.7143C21.7548 17 21.795 17.001 21.835 17.0032Z" fill="white" fill-opacity="0.4" style="mix-blend-mode:overlay"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,175 @@
%PDF-1.7
1 0 obj
<< /Type /XObject
/Length 2 0 R
/Group << /Type /Group
/S /Transparency
/I true
>>
/Subtype /Form
/Resources << /ExtGState << /E1 << /ca 0.400000 >> >> >>
/BBox [ 0.000000 0.000000 24.000000 24.000000 ]
>>
stream
q
/E1 gs
1.000000 0.000000 -0.000000 1.000000 16.000000 0.000000 cm
0.000000 0.000000 0.000000 scn
4.000000 11.165039 m
2.252019 11.165039 0.835000 9.748020 0.835000 8.000039 c
0.835000 6.469324 l
0.325283 6.041227 0.000000 5.392427 0.000000 4.666706 c
0.000000 2.333372 l
0.000000 1.045606 1.024229 0.000039 2.285714 0.000039 c
5.714286 0.000039 l
6.975772 0.000039 8.000000 1.045606 8.000000 2.333372 c
8.000000 4.666706 l
8.000000 5.392427 7.674717 6.041228 7.165000 6.469325 c
7.165000 8.000039 l
7.165000 9.748020 5.747981 11.165039 4.000000 11.165039 c
h
5.835000 6.996834 m
5.835000 8.000039 l
5.835000 9.013481 5.013443 9.835039 4.000000 9.835039 c
2.986557 9.835039 2.165000 9.013481 2.165000 8.000039 c
2.165000 6.996834 l
2.204976 6.998962 2.245223 7.000039 2.285714 7.000039 c
5.714286 7.000039 l
5.754777 7.000039 5.795024 6.998962 5.835000 6.996834 c
h
f*
n
Q
endstream
endobj
2 0 obj
874
endobj
3 0 obj
<< /XObject << /X1 1 0 R >>
/ExtGState << /E2 << /ca 1.000000 >>
/E1 << /BM /Overlay >>
>>
>>
endobj
4 0 obj
<< /Length 5 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 2.750000 5.669922 cm
0.000000 0.000000 0.000000 scn
1.720738 14.800344 m
1.461039 15.060041 1.039985 15.060041 0.780286 14.800344 c
0.520587 14.540645 0.520587 14.119590 0.780286 13.859891 c
10.960286 3.679891 l
11.174960 3.465217 11.499891 3.427998 11.752796 3.568234 c
11.755006 3.566025 l
11.758398 3.569099 11.761784 3.572182 11.765164 3.575269 c
11.813600 3.603508 11.859230 3.638382 11.900739 3.679891 c
11.942236 3.721388 11.977103 3.767005 12.005339 3.815426 c
12.611081 4.478601 12.980549 5.361266 12.980549 6.330225 c
12.980549 8.390247 11.310571 10.060225 9.250548 10.060225 c
8.460713 10.060225 7.728216 9.814731 7.125198 9.395884 c
5.928929 10.592153 l
6.895550 11.000750 8.003197 11.265054 9.251135 11.265054 c
13.367373 11.265054 15.957394 8.390061 16.996548 6.942309 c
17.210707 6.643942 17.626190 6.575678 17.924557 6.789837 c
18.222925 7.003996 18.291189 7.419480 18.077030 7.717847 c
16.964485 9.267849 14.028145 12.595054 9.251135 12.595054 c
7.585011 12.595054 6.142790 12.190375 4.923682 11.597401 c
1.720738 14.800344 l
h
8.089747 8.431334 m
11.351658 5.169424 l
11.542119 5.513433 11.650549 5.909166 11.650549 6.330225 c
11.650549 7.655708 10.576033 8.730225 9.250548 8.730225 c
8.829490 8.730225 8.433756 8.621795 8.089747 8.431334 c
h
3.024544 10.415583 m
1.729657 9.419269 0.839761 8.309132 0.352916 7.615991 c
-0.101896 6.968456 -0.116593 6.127693 0.305817 5.463812 c
1.319174 3.871166 4.252128 0.065054 9.251135 0.065054 c
10.013844 0.065054 10.731159 0.154280 11.402498 0.311954 c
11.760039 0.395927 11.981809 0.753845 11.897836 1.111385 c
11.813862 1.468926 11.455944 1.690696 11.098404 1.606723 c
10.524468 1.471926 9.909159 1.395054 9.251135 1.395054 c
4.977560 1.395054 2.390994 4.664186 1.427933 6.177784 c
1.292147 6.391191 1.298440 6.648187 1.441279 6.851551 c
1.914984 7.525985 2.760206 8.565481 3.973856 9.466270 c
3.024544 10.415583 l
h
5.768876 7.671250 m
5.608479 7.255088 5.520549 6.802925 5.520549 6.330225 c
5.520549 4.270203 7.190527 2.600224 9.250548 2.600224 c
9.723249 2.600224 10.175412 2.688154 10.591575 2.848551 c
9.497365 3.942760 l
9.416221 3.934471 9.333880 3.930224 9.250548 3.930224 c
7.925066 3.930224 6.850549 5.004741 6.850549 6.330225 c
6.850549 6.413557 6.854796 6.495897 6.863086 6.577041 c
5.768876 7.671250 l
h
f*
n
Q
q
/E1 gs
/E2 gs
/X1 Do
Q
endstream
endobj
5 0 obj
2376
endobj
6 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 3 0 R
/Contents 4 0 R
/Parent 7 0 R
>>
endobj
7 0 obj
<< /Kids [ 6 0 R ]
/Count 1
/Type /Pages
>>
endobj
8 0 obj
<< /Pages 7 0 R
/Type /Catalog
>>
endobj
xref
0 9
0000000000 65535 f
0000000010 00000 n
0000001195 00000 n
0000001217 00000 n
0000001371 00000 n
0000003803 00000 n
0000003826 00000 n
0000003999 00000 n
0000004073 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 8 0 R
/Size 9
>>
startxref
4132
%%EOF

View File

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

View File

@ -0,0 +1,165 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 2.335938 3.834961 cm
0.000000 0.000000 0.000000 scn
5.465000 16.330078 m
5.436178 16.330078 l
5.436147 16.330078 l
4.620521 16.330084 3.967866 16.330088 3.440454 16.286997 c
2.899074 16.242764 2.431364 16.149834 2.001125 15.930616 c
1.311511 15.579240 0.750837 15.018566 0.399462 14.328953 c
0.180244 13.898714 0.087314 13.431004 0.043081 12.889624 c
-0.000010 12.362204 -0.000006 11.709539 0.000000 10.893900 c
0.000000 10.865077 l
0.000000 5.465077 l
0.000000 5.436255 l
-0.000006 4.620615 -0.000010 3.967950 0.043081 3.440531 c
0.087314 2.899150 0.180244 2.431440 0.399462 2.001202 c
0.750837 1.311588 1.311511 0.750915 2.001125 0.399538 c
2.431364 0.180321 2.899074 0.087391 3.440454 0.043159 c
3.967844 0.000069 4.620466 0.000072 5.436043 0.000076 c
5.436195 0.000076 l
5.465001 0.000076 l
13.865001 0.000076 l
13.893806 0.000076 l
13.893958 0.000076 l
14.709535 0.000072 15.362156 0.000069 15.889547 0.043159 c
16.430927 0.087391 16.898638 0.180321 17.328876 0.399538 c
18.018492 0.750915 18.579165 1.311588 18.930540 2.001202 c
19.149757 2.431440 19.242687 2.899151 19.286921 3.440531 c
19.330011 3.967940 19.330008 4.620589 19.330002 5.436206 c
19.330002 5.436272 l
19.330002 5.465077 l
19.330002 10.865078 l
19.330002 10.893884 l
19.330002 10.893948 l
19.330008 11.709565 19.330011 12.362215 19.286921 12.889624 c
19.242687 13.431004 19.149757 13.898714 18.930540 14.328953 c
18.579165 15.018566 18.018492 15.579240 17.328876 15.930616 c
16.898638 16.149834 16.430927 16.242764 15.889547 16.286997 c
15.362134 16.330088 14.709479 16.330084 13.893853 16.330078 c
13.893822 16.330078 l
13.865000 16.330078 l
5.465000 16.330078 l
h
2.604933 14.745577 m
2.816429 14.853340 3.089627 14.923901 3.548759 14.961413 c
4.015654 14.999559 4.613948 15.000077 5.465000 15.000077 c
13.865000 15.000077 l
14.716052 15.000077 15.314347 14.999559 15.781242 14.961413 c
16.240376 14.923901 16.513573 14.853340 16.725069 14.745577 c
17.164429 14.521713 17.521637 14.164503 17.745502 13.725145 c
17.853264 13.513649 17.923824 13.240451 17.961338 12.781319 c
17.999485 12.314424 18.000002 11.716129 18.000002 10.865078 c
18.000002 5.465077 l
18.000002 4.614025 17.999485 4.015731 17.961338 3.548836 c
17.923824 3.089704 17.853264 2.816505 17.745502 2.605009 c
17.521637 2.165651 17.164429 1.808441 16.725069 1.584577 c
16.513573 1.476814 16.240376 1.406254 15.781242 1.368741 c
15.314347 1.330594 14.716053 1.330077 13.865001 1.330077 c
5.465001 1.330077 l
4.613949 1.330077 4.015654 1.330594 3.548759 1.368741 c
3.089627 1.406254 2.816429 1.476814 2.604933 1.584577 c
2.165574 1.808441 1.808365 2.165651 1.584500 2.605009 c
1.476737 2.816505 1.406177 3.089704 1.368664 3.548835 c
1.330518 4.015731 1.330000 4.614025 1.330000 5.465077 c
1.330000 10.865077 l
1.330000 11.716129 1.330518 12.314424 1.368664 12.781319 c
1.406177 13.240451 1.476737 13.513649 1.584500 13.725145 c
1.808365 14.164503 2.165574 14.521713 2.604933 14.745577 c
h
3.284180 5.076172 m
3.284180 4.661133 3.523438 4.421875 3.914063 4.421875 c
4.309570 4.421875 4.543945 4.661133 4.543945 5.076172 c
4.543945 7.561523 l
7.971680 7.561523 l
7.971680 5.076172 l
7.971680 4.661133 8.206055 4.421875 8.596680 4.421875 c
8.992188 4.421875 9.226562 4.661133 9.226562 5.076172 c
9.226562 10.979492 l
9.226562 11.394531 8.992188 11.633789 8.596680 11.633789 c
8.206055 11.633789 7.971680 11.394531 7.971680 10.979492 c
7.971680 8.621094 l
4.543945 8.621094 l
4.543945 10.979492 l
4.543945 11.394531 4.309570 11.633789 3.914063 11.633789 c
3.523438 11.633789 3.284180 11.394531 3.284180 10.979492 c
3.284180 5.076172 l
h
10.393680 5.159180 m
10.393680 4.749023 10.632937 4.504883 11.023562 4.504883 c
12.966922 4.504883 l
15.076297 4.504883 16.316532 5.813477 16.316532 8.044922 c
16.316532 10.276367 15.071414 11.550781 12.966922 11.550781 c
11.023562 11.550781 l
10.632937 11.550781 10.393680 11.306641 10.393680 10.896484 c
10.393680 5.159180 l
h
12.835086 5.569336 m
11.653445 5.569336 l
11.653445 10.486328 l
12.835086 10.486328 l
14.241336 10.486328 15.032351 9.617188 15.032351 8.040039 c
15.032351 6.433594 14.251101 5.569336 12.835086 5.569336 c
h
f*
n
Q
endstream
endobj
3 0 obj
4179
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000004269 00000 n
0000004292 00000 n
0000004465 00000 n
0000004539 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
4598
%%EOF

View File

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

View File

@ -0,0 +1,221 @@
%PDF-1.7
1 0 obj
<< /Type /XObject
/Length 2 0 R
/Group << /Type /Group
/S /Transparency
/I true
>>
/Subtype /Form
/Resources << /ExtGState << /E1 << /ca 0.400000 >> >> >>
/BBox [ 0.000000 0.000000 24.000000 24.000000 ]
>>
stream
q
/E1 gs
1.000000 0.000000 -0.000000 1.000000 16.000000 0.000000 cm
0.000000 0.000000 0.000000 scn
4.000000 11.165039 m
2.252019 11.165039 0.835000 9.748020 0.835000 8.000039 c
0.835000 6.469324 l
0.325283 6.041227 0.000000 5.392427 0.000000 4.666706 c
0.000000 2.333372 l
0.000000 1.045606 1.024229 0.000039 2.285714 0.000039 c
5.714286 0.000039 l
6.975772 0.000039 8.000000 1.045606 8.000000 2.333372 c
8.000000 4.666706 l
8.000000 5.392427 7.674717 6.041228 7.165000 6.469325 c
7.165000 8.000039 l
7.165000 9.748020 5.747981 11.165039 4.000000 11.165039 c
h
5.835000 6.996834 m
5.835000 8.000039 l
5.835000 9.013481 5.013443 9.835039 4.000000 9.835039 c
2.986557 9.835039 2.165000 9.013481 2.165000 8.000039 c
2.165000 6.996834 l
2.204976 6.998962 2.245223 7.000039 2.285714 7.000039 c
5.714286 7.000039 l
5.754777 7.000039 5.795024 6.998962 5.835000 6.996834 c
h
f*
n
Q
endstream
endobj
2 0 obj
874
endobj
3 0 obj
<< /XObject << /X1 1 0 R >>
/ExtGState << /E2 << /ca 1.000000 >>
/E1 << /BM /Overlay >>
>>
>>
endobj
4 0 obj
<< /Length 5 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
/E1 gs
/E2 gs
/X1 Do
Q
q
1.000000 0.000000 -0.000000 1.000000 2.333984 3.834961 cm
0.000000 0.000000 0.000000 scn
5.436178 16.330078 m
5.465000 16.330078 l
13.865001 16.330078 l
13.893823 16.330078 l
13.893857 16.330078 l
13.893866 16.330078 l
14.709486 16.330084 15.362138 16.330088 15.889548 16.286995 c
16.430927 16.242764 16.898638 16.149834 17.328876 15.930615 c
18.018492 15.579240 18.579165 15.018566 18.930540 14.328952 c
19.149757 13.898713 19.242687 13.431004 19.286921 12.889624 c
19.330011 12.362215 19.330008 11.709565 19.330002 10.893948 c
19.330002 10.893884 l
19.330002 10.865078 l
19.330002 8.341689 l
18.913713 8.507795 18.466766 8.613466 18.000002 8.647859 c
18.000002 10.865078 l
18.000002 11.716129 17.999485 12.314424 17.961338 12.781319 c
17.923824 13.240451 17.853264 13.513648 17.745502 13.725145 c
17.521637 14.164503 17.164429 14.521712 16.725069 14.745577 c
16.513573 14.853340 16.240376 14.923901 15.781243 14.961412 c
15.314348 14.999559 14.716053 15.000076 13.865001 15.000076 c
5.465000 15.000076 l
4.613949 15.000076 4.015654 14.999559 3.548759 14.961412 c
3.089627 14.923901 2.816429 14.853340 2.604933 14.745577 c
2.165575 14.521712 1.808365 14.164503 1.584500 13.725145 c
1.476738 13.513648 1.406177 13.240451 1.368665 12.781319 c
1.330518 12.314424 1.330001 11.716129 1.330001 10.865077 c
1.330001 5.465077 l
1.330001 4.614025 1.330518 4.015731 1.368665 3.548835 c
1.406177 3.089704 1.476738 2.816505 1.584500 2.605009 c
1.808365 2.165651 2.165575 1.808441 2.604933 1.584577 c
2.816429 1.476814 3.089627 1.406254 3.548759 1.368741 c
4.015654 1.330594 4.613949 1.330077 5.465001 1.330077 c
12.368263 1.330077 l
12.346300 1.167022 12.334961 1.000680 12.334961 0.831823 c
12.334961 0.000076 l
5.465001 0.000076 l
5.436195 0.000076 l
4.620547 0.000071 3.967877 0.000067 3.440454 0.043158 c
2.899074 0.087391 2.431364 0.180321 2.001126 0.399538 c
1.311511 0.750915 0.750838 1.311588 0.399462 2.001202 c
0.180244 2.431440 0.087314 2.899150 0.043082 3.440531 c
-0.000010 3.967947 -0.000005 4.620607 0.000000 5.436243 c
0.000000 5.436255 l
0.000000 5.465077 l
0.000000 10.865077 l
0.000000 10.893900 l
0.000000 10.893912 l
-0.000005 11.709545 -0.000010 12.362206 0.043082 12.889624 c
0.087314 13.431004 0.180244 13.898713 0.399462 14.328952 c
0.750838 15.018566 1.311511 15.579240 2.001126 15.930615 c
2.431364 16.149834 2.899074 16.242764 3.440454 16.286995 c
3.967864 16.330088 4.620515 16.330084 5.436135 16.330078 c
5.436144 16.330078 l
5.436178 16.330078 l
h
16.302193 8.449865 m
16.158422 10.429876 14.940601 11.550781 12.966923 11.550781 c
11.023563 11.550781 l
10.632938 11.550781 10.393681 11.306641 10.393681 10.896484 c
10.393681 5.159180 l
10.393681 4.749023 10.632938 4.504883 11.023563 4.504883 c
12.966923 4.504883 l
13.039984 4.504883 13.112001 4.506454 13.182961 4.509577 c
13.212296 4.896732 13.290678 5.270188 13.411903 5.623747 c
13.233344 5.587598 13.040921 5.569336 12.835087 5.569336 c
11.653446 5.569336 l
11.653446 10.486328 l
12.835087 10.486328 l
14.241337 10.486328 15.032352 9.617188 15.032352 8.040039 c
15.032352 7.959743 15.030400 7.881300 15.026515 7.804728 c
15.410401 8.083501 15.840104 8.303026 16.302193 8.449865 c
h
17.996735 4.495156 m
18.047235 4.495156 l
18.031771 4.513056 18.015068 4.529845 17.997263 4.545417 c
17.997097 4.528533 17.996922 4.511781 17.996735 4.495156 c
h
3.914063 4.421875 m
3.523438 4.421875 3.284180 4.661133 3.284180 5.076172 c
3.284180 10.979492 l
3.284180 11.394531 3.523438 11.633789 3.914063 11.633789 c
4.309571 11.633789 4.543946 11.394531 4.543946 10.979492 c
4.543946 8.621094 l
7.971680 8.621094 l
7.971680 10.979492 l
7.971680 11.394531 8.206055 11.633789 8.596680 11.633789 c
8.992188 11.633789 9.226562 11.394531 9.226562 10.979492 c
9.226562 5.076172 l
9.226562 4.661133 8.992188 4.421875 8.596680 4.421875 c
8.206055 4.421875 7.971680 4.661133 7.971680 5.076172 c
7.971680 7.561523 l
4.543946 7.561523 l
4.543946 5.076172 l
4.543946 4.661133 4.309571 4.421875 3.914063 4.421875 c
h
f*
n
Q
endstream
endobj
5 0 obj
4026
endobj
6 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 3 0 R
/Contents 4 0 R
/Parent 7 0 R
>>
endobj
7 0 obj
<< /Kids [ 6 0 R ]
/Count 1
/Type /Pages
>>
endobj
8 0 obj
<< /Pages 7 0 R
/Type /Catalog
>>
endobj
xref
0 9
0000000000 65535 f
0000000010 00000 n
0000001195 00000 n
0000001217 00000 n
0000001371 00000 n
0000005453 00000 n
0000005476 00000 n
0000005649 00000 n
0000005723 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 8 0 R
/Size 9
>>
startxref
5782
%%EOF

View File

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

View File

@ -0,0 +1,169 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 2.335938 3.834961 cm
0.000000 0.000000 0.000000 scn
5.465000 16.330078 m
5.436178 16.330078 l
5.436147 16.330078 l
4.620521 16.330084 3.967866 16.330088 3.440454 16.286997 c
2.899074 16.242764 2.431364 16.149834 2.001125 15.930616 c
1.311511 15.579240 0.750837 15.018566 0.399462 14.328953 c
0.180244 13.898714 0.087314 13.431004 0.043081 12.889624 c
-0.000010 12.362204 -0.000006 11.709539 0.000000 10.893900 c
0.000000 10.865077 l
0.000000 5.465077 l
0.000000 5.436255 l
-0.000006 4.620615 -0.000010 3.967950 0.043081 3.440531 c
0.087314 2.899150 0.180244 2.431440 0.399462 2.001202 c
0.750837 1.311588 1.311511 0.750915 2.001125 0.399538 c
2.431364 0.180321 2.899074 0.087391 3.440454 0.043159 c
3.967844 0.000069 4.620466 0.000072 5.436043 0.000076 c
5.436195 0.000076 l
5.465001 0.000076 l
13.865001 0.000076 l
13.893806 0.000076 l
13.893958 0.000076 l
14.709535 0.000072 15.362156 0.000069 15.889547 0.043159 c
16.430927 0.087391 16.898638 0.180321 17.328876 0.399538 c
18.018492 0.750915 18.579165 1.311588 18.930540 2.001202 c
19.149757 2.431440 19.242687 2.899151 19.286921 3.440531 c
19.330011 3.967940 19.330008 4.620589 19.330002 5.436206 c
19.330002 5.436272 l
19.330002 5.465077 l
19.330002 10.865078 l
19.330002 10.893884 l
19.330002 10.893948 l
19.330008 11.709565 19.330011 12.362215 19.286921 12.889624 c
19.242687 13.431004 19.149757 13.898714 18.930540 14.328953 c
18.579165 15.018566 18.018492 15.579240 17.328876 15.930616 c
16.898638 16.149834 16.430927 16.242764 15.889547 16.286997 c
15.362134 16.330088 14.709479 16.330084 13.893853 16.330078 c
13.893822 16.330078 l
13.865000 16.330078 l
5.465000 16.330078 l
h
2.604933 14.745577 m
2.816429 14.853340 3.089627 14.923901 3.548759 14.961413 c
4.015654 14.999559 4.613948 15.000077 5.465000 15.000077 c
13.865000 15.000077 l
14.716052 15.000077 15.314347 14.999559 15.781242 14.961413 c
16.240376 14.923901 16.513573 14.853340 16.725069 14.745577 c
17.164429 14.521713 17.521637 14.164503 17.745502 13.725145 c
17.853264 13.513649 17.923824 13.240451 17.961338 12.781319 c
17.999485 12.314424 18.000002 11.716129 18.000002 10.865078 c
18.000002 5.465077 l
18.000002 4.614025 17.999485 4.015731 17.961338 3.548836 c
17.923824 3.089704 17.853264 2.816505 17.745502 2.605009 c
17.521637 2.165651 17.164429 1.808441 16.725069 1.584577 c
16.513573 1.476814 16.240376 1.406254 15.781242 1.368741 c
15.314347 1.330594 14.716053 1.330077 13.865001 1.330077 c
5.465001 1.330077 l
4.613949 1.330077 4.015654 1.330594 3.548759 1.368741 c
3.089627 1.406254 2.816429 1.476814 2.604933 1.584577 c
2.165574 1.808441 1.808365 2.165651 1.584500 2.605009 c
1.476737 2.816505 1.406177 3.089704 1.368664 3.548835 c
1.330518 4.015731 1.330000 4.614025 1.330000 5.465077 c
1.330000 10.865077 l
1.330000 11.716129 1.330518 12.314424 1.368664 12.781319 c
1.406177 13.240451 1.476737 13.513649 1.584500 13.725145 c
1.808365 14.164503 2.165574 14.521713 2.604933 14.745577 c
h
3.647461 5.774414 m
3.920899 4.973633 4.858398 4.387695 6.264648 4.387695 c
7.958984 4.387695 9.018555 5.237305 9.018555 6.584961 c
9.018555 7.629883 8.393555 8.225586 6.909180 8.533203 c
6.152344 8.689453 l
5.273438 8.875000 4.921875 9.182617 4.921875 9.661133 c
4.921875 10.251953 5.478516 10.623047 6.259766 10.623047 c
6.918945 10.623047 7.421875 10.369141 7.661133 9.773438 c
7.792969 9.500000 7.973633 9.373047 8.266602 9.373047 c
8.593750 9.373047 8.813477 9.583008 8.813477 9.890625 c
8.813477 10.007812 8.798828 10.100586 8.769531 10.178711 c
8.481445 11.111328 7.514648 11.667969 6.269531 11.667969 c
4.741211 11.667969 3.652344 10.828125 3.652344 9.587891 c
3.652344 8.557617 4.311523 7.893555 5.722656 7.605469 c
6.479492 7.449219 l
7.397461 7.258789 7.749023 6.946289 7.749023 6.438477 c
7.749023 5.852539 7.153320 5.432617 6.303711 5.432617 c
5.551758 5.432617 4.946289 5.706055 4.726562 6.316406 c
4.589844 6.599609 4.414062 6.711914 4.135742 6.711914 c
3.803711 6.711914 3.579102 6.487305 3.579102 6.135742 c
3.579102 6.018555 3.603516 5.891602 3.647461 5.774414 c
h
9.887820 5.159180 m
9.887820 4.749023 10.127078 4.504883 10.517703 4.504883 c
12.461062 4.504883 l
14.570437 4.504883 15.810672 5.813477 15.810672 8.044922 c
15.810672 10.276367 14.565555 11.550781 12.461062 11.550781 c
10.517703 11.550781 l
10.127078 11.550781 9.887820 11.306641 9.887820 10.896484 c
9.887820 5.159180 l
h
12.329226 5.569336 m
11.147586 5.569336 l
11.147586 10.486328 l
12.329226 10.486328 l
13.735476 10.486328 14.526492 9.617188 14.526492 8.040039 c
14.526492 6.433594 13.745242 5.569336 12.329226 5.569336 c
h
f*
n
Q
endstream
endobj
3 0 obj
4613
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000004703 00000 n
0000004726 00000 n
0000004899 00000 n
0000004973 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
5032
%%EOF

View File

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

View File

@ -0,0 +1,115 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 2.334961 4.835083 cm
0.000000 0.000000 0.000000 scn
0.000000 10.664956 m
0.000000 12.689079 1.640877 14.329956 3.665000 14.329956 c
11.913673 14.329956 l
13.215667 14.329956 14.458418 13.785840 15.341534 12.829130 c
18.901438 8.972565 l
19.843767 7.951705 19.843767 6.378191 18.901434 5.357332 c
15.341534 1.500778 l
14.458418 0.544071 13.215668 -0.000044 11.913676 -0.000044 c
3.665000 -0.000044 l
1.640876 -0.000044 0.000000 1.640832 0.000000 3.664956 c
0.000000 10.664956 l
h
3.665000 12.999956 m
2.375415 12.999956 1.330000 11.954540 1.330000 10.664956 c
1.330000 3.664956 l
1.330000 2.375371 2.375415 1.329956 3.665000 1.329956 c
11.913676 1.329956 l
12.844467 1.329956 13.732907 1.718943 14.364245 2.402891 c
17.924147 6.259445 l
18.396196 6.770833 18.396196 7.559066 17.924149 8.070454 c
14.364244 11.927019 l
13.732906 12.610969 12.844466 12.999956 11.913673 12.999956 c
3.665000 12.999956 l
h
7.165039 11.829978 m
7.442241 11.829978 7.690366 11.658028 7.787698 11.398476 c
9.662698 6.398475 l
10.787698 3.398476 l
10.916655 3.054590 10.742421 2.671276 10.398537 2.542319 c
10.054651 2.413363 9.671337 2.587596 9.542380 2.931481 c
8.579194 5.499978 l
5.750885 5.499978 l
4.787698 2.931481 l
4.658741 2.587596 4.275427 2.413363 3.931542 2.542319 c
3.587657 2.671276 3.413423 3.054590 3.542380 3.398476 c
4.667380 6.398475 l
6.542380 11.398476 l
6.639712 11.658028 6.887837 11.829978 7.165039 11.829978 c
h
6.249635 6.829978 m
8.080444 6.829978 l
7.165039 9.271057 l
6.249635 6.829978 l
h
13.665039 5.664963 m
14.493466 5.664963 15.165039 6.336535 15.165039 7.164963 c
15.165039 7.993390 14.493466 8.664963 13.665039 8.664963 c
12.836612 8.664963 12.165039 7.993390 12.165039 7.164963 c
12.165039 6.336535 12.836612 5.664963 13.665039 5.664963 c
h
f*
n
Q
endstream
endobj
3 0 obj
1828
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001918 00000 n
0000001941 00000 n
0000002114 00000 n
0000002188 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
2247
%%EOF

View File

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

View File

@ -0,0 +1,118 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 3.084961 6.136719 cm
0.000000 0.000000 0.000000 scn
18.942936 18.774218 m
19.021086 18.696367 19.147511 18.696487 19.225510 18.774488 c
19.613930 19.162907 l
19.906822 19.455801 19.906822 19.930674 19.613930 20.223568 c
19.321035 20.516460 18.846163 20.516460 18.553268 20.223568 c
18.163366 19.833666 l
18.085157 19.755455 18.085278 19.628614 18.163637 19.550552 c
18.942936 18.774218 l
h
8.613929 8.162907 m
18.472507 18.021486 l
18.550613 18.099590 18.550613 18.226223 18.472507 18.304329 c
17.694691 19.082146 l
17.616585 19.160252 17.489952 19.160252 17.411848 19.082146 c
7.553268 9.223567 l
7.353394 9.023692 6.960623 8.136981 6.731307 7.593929 c
6.662903 7.431941 6.822302 7.272542 6.984291 7.340945 c
7.527342 7.570261 8.414054 7.963032 8.613929 8.162907 c
h
4.415000 16.323277 m
2.711202 16.323277 1.330000 14.942075 1.330000 13.238276 c
1.330000 4.488276 l
1.330000 2.784477 2.711201 1.403276 4.415000 1.403276 c
14.725845 1.403276 l
15.935735 1.403276 17.090574 1.908901 17.911217 2.797930 c
22.361094 7.618624 l
23.009941 8.321542 23.009941 9.404994 22.361095 10.107914 c
20.176357 12.474716 l
18.301357 14.505968 l
18.052244 14.775839 18.069075 15.196557 18.338945 15.445669 c
18.608816 15.694780 19.029533 15.677951 19.278645 15.408080 c
21.153645 13.376827 l
23.338383 11.010025 l
24.457512 9.797634 24.457512 7.928901 23.338383 6.716511 c
18.888506 1.895817 l
17.816084 0.734028 16.306934 0.073275 14.725845 0.073275 c
4.415000 0.073275 l
1.976663 0.073275 0.000000 2.049938 0.000000 4.488276 c
0.000000 13.238276 l
0.000000 15.676614 1.976663 17.653276 4.415000 17.653276 c
8.790000 17.653276 l
11.852500 17.653276 l
12.219769 17.653276 12.517500 17.355545 12.517500 16.988277 c
12.517500 16.621006 12.219769 16.323277 11.852500 16.323277 c
8.790000 16.323277 l
4.415000 16.323277 l
h
18.790039 8.863276 m
18.790039 7.827743 17.950573 6.988276 16.915039 6.988276 c
15.879504 6.988276 15.040038 7.827743 15.040038 8.863276 c
15.040038 9.898810 15.879504 10.738276 16.915039 10.738276 c
17.950573 10.738276 18.790039 9.898810 18.790039 8.863276 c
h
f*
n
Q
endstream
endobj
3 0 obj
2139
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000002229 00000 n
0000002252 00000 n
0000002425 00000 n
0000002499 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
2558
%%EOF

View File

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

View File

@ -0,0 +1,113 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.839844 3.335083 cm
0.000000 0.000000 0.000000 scn
2.244934 16.039955 m
2.244934 18.478292 4.221597 20.454956 6.659934 20.454956 c
16.970776 20.454956 l
18.551868 20.454956 20.061018 19.794203 21.133440 18.632410 c
25.583317 13.811706 l
26.702446 12.599316 26.702448 10.730581 25.583317 9.518190 c
21.133440 4.697496 l
20.061018 3.535707 18.551868 2.874954 16.970779 2.874954 c
11.659995 2.874954 l
11.292726 2.874954 10.994995 3.172686 10.994995 3.539955 c
10.994995 3.907225 11.292726 4.204956 11.659995 4.204956 c
16.970779 4.204956 l
18.180668 4.204956 19.335508 4.710580 20.156151 5.599609 c
24.606028 10.420303 l
25.254875 11.123221 25.254875 12.206675 24.606028 12.909594 c
20.156151 17.730299 l
19.335506 18.619331 18.180668 19.124956 16.970776 19.124956 c
6.659934 19.124956 l
4.956136 19.124956 3.574934 17.743755 3.574934 16.039955 c
3.574934 11.664956 l
3.574934 11.297687 3.277204 10.999956 2.909934 10.999956 c
2.542665 10.999956 2.244934 11.297687 2.244934 11.664956 c
2.244934 16.039955 l
h
21.034973 11.664956 m
21.034973 10.629422 20.195507 9.789956 19.159973 9.789956 c
18.124439 9.789956 17.284973 10.629422 17.284973 11.664956 c
17.284973 12.700490 18.124439 13.539956 19.159973 13.539956 c
20.195507 13.539956 21.034973 12.700490 21.034973 11.664956 c
h
4.665000 9.329956 m
5.032269 9.329956 5.330000 9.032225 5.330000 8.664955 c
5.330000 5.329955 l
8.665000 5.329955 l
9.032269 5.329955 9.330000 5.032225 9.330000 4.664955 c
9.330000 4.297686 9.032269 3.999954 8.665000 3.999954 c
5.330000 3.999954 l
5.330000 0.664955 l
5.330000 0.297686 5.032269 -0.000046 4.665000 -0.000046 c
4.297730 -0.000046 4.000000 0.297686 4.000000 0.664955 c
4.000000 3.999954 l
0.665000 3.999954 l
0.297731 3.999954 0.000000 4.297686 0.000000 4.664955 c
0.000000 5.032225 0.297731 5.329955 0.665000 5.329955 c
4.000000 5.329955 l
4.000000 8.664955 l
4.000000 9.032225 4.297730 9.329956 4.665000 9.329956 c
h
f*
n
Q
endstream
endobj
3 0 obj
1984
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000002074 00000 n
0000002097 00000 n
0000002270 00000 n
0000002344 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
2403
%%EOF

View File

@ -15,6 +15,7 @@ import EntityKeyboard
import TextNodeWithEntities
import PremiumUI
import TooltipUI
import TopMessageReactions
extension ChatControllerImpl {
func openMessageContextMenu(message: Message, selectAll: Bool, node: ASDisplayNode, frame: CGRect, anyRecognizer: UIGestureRecognizer?, location: CGPoint?) -> Void {
@ -110,11 +111,18 @@ extension ChatControllerImpl {
if canAddMessageReactions(message: topMessage), let allowedReactions = allowedReactions, !topReactions.isEmpty {
actions.reactionItems = topReactions.map(ReactionContextItem.reaction)
actions.selectedReactionItems = selectedReactions.reactions
if message.areReactionsTags(accountPeerId: self.context.account.peerId) {
actions.reactionsTitle = presentationData.strings.Chat_ContextMenuTagsTitle
if self.presentationInterfaceState.isPremium {
actions.reactionsTitle = presentationData.strings.Chat_ContextMenuTagsTitle
} else {
//TODO:localize
actions.reactionsTitle = "Organize your Saved Messages with tags for quicker access. [Learn more...]()"
actions.reactionsLocked = true
actions.selectedReactionItems = Set()
}
actions.allPresetReactionsAreAvailable = true
}
actions.selectedReactionItems = selectedReactions.reactions
if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, case .broadcast = channel.info {
actions.alwaysAllowPremiumReactions = true
@ -299,6 +307,7 @@ extension ChatControllerImpl {
}
self.currentContextController = controller
//TODO:localize
controller.premiumReactionsSelected = { [weak self, weak controller] in
guard let self else {
return

View File

@ -121,6 +121,7 @@ import MediaEditorScreen
import WallpaperGalleryScreen
import WallpaperGridScreen
import VideoMessageCameraScreen
import TopMessageReactions
public enum ChatControllerPeekActions {
case standard
@ -1253,7 +1254,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return
}
self.openMessageReactionContextMenu(message: message, sourceView: sourceView, gesture: gesture, value: value)
}, updateMessageReaction: { [weak self] initialMessage, reaction in
}, updateMessageReaction: { [weak self] initialMessage, reaction, force in
guard let strongSelf = self else {
return
}
@ -1264,6 +1265,54 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return
}
if !force && message.areReactionsTags(accountPeerId: strongSelf.context.account.peerId) {
strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in
guard let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item else {
return
}
guard item.message.id == message.id else {
return
}
let chosenReaction: MessageReaction.Reaction?
switch reaction {
case .default:
switch item.associatedData.defaultReaction {
case .none:
chosenReaction = nil
case let .builtin(value):
chosenReaction = .builtin(value)
case let .custom(fileId):
chosenReaction = .custom(fileId)
}
case let .reaction(value):
switch value {
case let .builtin(value):
chosenReaction = .builtin(value)
case let .custom(fileId):
chosenReaction = .custom(fileId)
}
}
guard let chosenReaction = chosenReaction else {
return
}
let tags: [EngineMessage.CustomTag] = [ReactionsMessageAttribute.messageTag(reaction: chosenReaction)]
if strongSelf.presentationInterfaceState.historyFilter?.customTags == tags {
strongSelf.interfaceInteraction?.updateHistoryFilter { _ in
return nil
}
} else {
strongSelf.interfaceInteraction?.updateHistoryFilter { _ in
return ChatPresentationInterfaceState.HistoryFilter(customTags: tags, isActive: true)
}
}
}
return
}
let _ = (peerMessageAllowedReactions(context: strongSelf.context, message: message)
|> deliverOnMainQueue).startStandalone(next: { allowedReactions in
guard let strongSelf = self else {
@ -8551,7 +8600,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}, deleteSelectedMessages: { [weak self] in
if let strongSelf = self {
if let messageIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds, !messageIds.isEmpty {
strongSelf.messageContextDisposable.set((strongSelf.context.sharedContext.chatAvailableMessageActions(engine: strongSelf.context.engine, accountPeerId: strongSelf.context.account.peerId, messageIds: messageIds)
strongSelf.messageContextDisposable.set((strongSelf.context.sharedContext.chatAvailableMessageActions(engine: strongSelf.context.engine, accountPeerId: strongSelf.context.account.peerId, messageIds: messageIds, keepUpdated: false)
|> deliverOnMainQueue).startStrict(next: { actions in
if let strongSelf = self, !actions.options.isEmpty {
if let banAuthor = actions.banAuthor {
@ -8665,7 +8714,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}, deleteMessages: { [weak self] messages, contextController, completion in
if let strongSelf = self, !messages.isEmpty {
let messageIds = Set(messages.map { $0.id })
strongSelf.messageContextDisposable.set((strongSelf.context.sharedContext.chatAvailableMessageActions(engine: strongSelf.context.engine, accountPeerId: strongSelf.context.account.peerId, messageIds: messageIds)
strongSelf.messageContextDisposable.set((strongSelf.context.sharedContext.chatAvailableMessageActions(engine: strongSelf.context.engine, accountPeerId: strongSelf.context.account.peerId, messageIds: messageIds, keepUpdated: false)
|> deliverOnMainQueue).startStrict(next: { actions in
if let strongSelf = self, !actions.options.isEmpty {
if let banAuthor = actions.banAuthor {
@ -18000,64 +18049,6 @@ final class ChatControllerContextReferenceContentSource: ContextReferenceContent
}
}
enum AllowedReactions {
case set(Set<MessageReaction.Reaction>)
case all
}
func peerMessageAllowedReactions(context: AccountContext, message: Message) -> Signal<AllowedReactions?, NoError> {
if message.id.peerId == context.account.peerId {
return .single(.all)
}
if message.containsSecretMedia {
return .single(AllowedReactions.set(Set()))
}
return combineLatest(
context.engine.data.get(
TelegramEngine.EngineData.Item.Peer.Peer(id: message.id.peerId),
TelegramEngine.EngineData.Item.Peer.AllowedReactions(id: message.id.peerId)
),
context.engine.stickers.availableReactions() |> take(1)
)
|> map { data, availableReactions -> AllowedReactions? in
let (peer, allowedReactions) = data
if let effectiveReactions = message.effectiveReactions(isTags: message.areReactionsTags(accountPeerId: context.account.peerId)), effectiveReactions.count >= 11 {
return .set(Set(effectiveReactions.map(\.value)))
}
switch allowedReactions {
case .unknown:
if case let .channel(channel) = peer, case .broadcast = channel.info {
if let availableReactions = availableReactions {
return .set(Set(availableReactions.reactions.map(\.value)))
} else {
return .set(Set())
}
}
return .all
case let .known(value):
switch value {
case .all:
if case let .channel(channel) = peer, case .broadcast = channel.info {
if let availableReactions = availableReactions {
return .set(Set(availableReactions.reactions.map(\.value)))
} else {
return .set(Set())
}
}
return .all
case let .limited(reactions):
return .set(Set(reactions))
case .empty:
return .set(Set())
}
}
}
}
func peerMessageSelectedReactions(context: AccountContext, message: Message) -> Signal<(reactions: Set<MessageReaction.Reaction>, files: Set<MediaId>), NoError> {
return context.engine.stickers.availableReactions()
|> take(1)

View File

@ -12,6 +12,7 @@ import UndoUI
import ChatInterfaceState
import PremiumUI
import ReactionSelectionNode
import TopMessageReactions
extension ChatControllerImpl {
func forwardMessages(messageIds: [MessageId], options: ChatInterfaceForwardOptionsState? = nil, resetCurrent: Bool = false) {

View File

@ -921,10 +921,6 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
return true
}
if strongSelf.chatPresentationInterfaceState.search != nil {
return true
}
return false
}

View File

@ -13,90 +13,137 @@ import TooltipUI
import StickerPackPreviewUI
import TextNodeWithEntities
import ChatPresentationInterfaceState
import PromptUI
import SavedTagNameAlertController
extension ChatControllerImpl {
func openMessageReactionContextMenu(message: Message, sourceView: ContextExtractedContentContainingView, gesture: ContextGesture?, value: MessageReaction.Reaction) {
if !self.chatDisplayNode.historyNode.rotated {
gesture?.cancel()
return
}
if message.areReactionsTags(accountPeerId: self.context.account.peerId) {
var items: [ContextMenuItem] = []
let reactionFile: Signal<TelegramMediaFile?, NoError>
switch value {
case .builtin:
reactionFile = self.context.engine.stickers.availableReactions()
|> take(1)
|> map { availableReactions -> TelegramMediaFile? in
return availableReactions?.reactions.first(where: { $0.value == value })?.selectAnimation
}
case let .custom(fileId):
reactionFile = self.context.engine.stickers.resolveInlineStickers(fileIds: [fileId])
|> map { files -> TelegramMediaFile? in
return files.values.first
}
}
let tags: [EngineMessage.CustomTag] = [ReactionsMessageAttribute.messageTag(reaction: value)]
if self.presentationInterfaceState.historyFilter?.customTags != tags {
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Chat_ReactionContextMenu_FilterByTag, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/TagFilter"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
let _ = (combineLatest(queue: .mainQueue(),
self.context.engine.stickers.savedMessageTagData(),
reactionFile
)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] savedMessageTags, reactionFile in
guard let self, let savedMessageTags else {
return
}
guard let reactionFile else {
return
}
var items: [ContextMenuItem] = []
let tags: [EngineMessage.CustomTag] = [ReactionsMessageAttribute.messageTag(reaction: value)]
var hasTitle = false
if let tag = savedMessageTags.tags.first(where: { $0.reaction == value }) {
if let title = tag.title, !title.isEmpty {
hasTitle = true
}
}
let optionTitle = hasTitle ? "Edit Name" : "Add Name"
//TODO:localize
items.append(.action(ContextMenuActionItem(text: optionTitle, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/TagEditName"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, a in
guard let self else {
a(.default)
return
}
self.chatDisplayNode.historyNode.frozenMessageForScrollingReset = message.id
self.interfaceInteraction?.updateHistoryFilter { _ in
return ChatPresentationInterfaceState.HistoryFilter(customTags: tags, isActive: true)
}
a(.default)
})))
}
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Edit Title", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
guard let self else {
a(.default)
return
}
a(.default)
let _ = (self.context.engine.stickers.savedMessageTagData()
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] savedMessageTags in
guard let self, let savedMessageTags else {
return
}
let reaction = value
//TODO:localize
let promptController = promptController(sharedContext: self.context.sharedContext, updatedPresentationData: nil, text: "Edit Title", value: savedMessageTags.tags.first(where: { $0.reaction == reaction })?.title ?? "", characterLimit: 10, apply: { [weak self] value in
c.dismiss(completion: { [weak self] in
guard let self else {
return
}
if let value {
let _ = self.context.engine.stickers.setSavedMessageTagTitle(reaction: reaction, title: value.isEmpty ? nil : value).start()
}
let _ = (self.context.engine.stickers.savedMessageTagData()
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] savedMessageTags in
guard let self, let savedMessageTags else {
return
}
let reaction = value
//TODO:localize
let promptController = savedTagNameAlertController(context: self.context, updatedPresentationData: nil, text: optionTitle, subtext: "You can label your emoji tag with a text name.", value: savedMessageTags.tags.first(where: { $0.reaction == reaction })?.title ?? "", reaction: reaction, file: reactionFile, characterLimit: 10, apply: { [weak self] value in
guard let self else {
return
}
if let value {
let _ = self.context.engine.stickers.setSavedMessageTagTitle(reaction: reaction, title: value.isEmpty ? nil : value).start()
}
})
self.interfaceInteraction?.presentController(promptController, nil)
})
})
self.interfaceInteraction?.presentController(promptController, nil)
})))
if self.presentationInterfaceState.historyFilter?.customTags != tags {
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Chat_ReactionContextMenu_FilterByTag, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/TagFilter"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, a in
guard let self else {
a(.default)
return
}
self.chatDisplayNode.historyNode.frozenMessageForScrollingReset = message.id
self.interfaceInteraction?.updateHistoryFilter { _ in
return ChatPresentationInterfaceState.HistoryFilter(customTags: tags, isActive: true)
}
a(.default)
})))
}
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Chat_ReactionContextMenu_RemoveTag, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/TagRemove"), color: theme.contextMenu.destructiveColor)
}, action: { [weak self] _, a in
a(.dismissWithoutContent)
guard let self else {
return
}
self.controllerInteraction?.updateMessageReaction(message, .reaction(value), true)
})))
self.canReadHistory.set(false)
let controller = ContextController(presentationData: self.presentationData, source: .extracted(ChatMessageReactionContextExtractedContentSource(chatNode: self.chatDisplayNode, engine: self.context.engine, message: message, contentView: sourceView)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture)
controller.dismissed = { [weak self] in
self?.canReadHistory.set(true)
}
self.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss()
}
return true
})
})))
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Chat_ReactionContextMenu_RemoveTag, textColor: .destructive, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/TagRemove"), color: theme.contextMenu.destructiveColor)
}, action: { [weak self] _, a in
a(.dismissWithoutContent)
guard let self else {
return
}
self.controllerInteraction?.updateMessageReaction(message, .reaction(value))
})))
self.canReadHistory.set(false)
let controller = ContextController(presentationData: self.presentationData, source: .extracted(ChatMessageReactionContextExtractedContentSource(chatNode: self.chatDisplayNode, engine: self.context.engine, message: message, contentView: sourceView)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture)
controller.dismissed = { [weak self] in
self?.canReadHistory.set(true)
}
self.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss()
}
return true
self.window?.presentInGlobalOverlay(controller)
})
self.window?.presentInGlobalOverlay(controller)
} else {
var customFileIds: [Int64] = []
if case let .custom(fileId) = value {

View File

@ -13,8 +13,12 @@ import ShareController
import ChatQrCodeScreen
import ChatShareMessageTagView
import ReactionSelectionNode
import TopMessageReactions
func chatShareToSavedMessagesAdditionalView(_ chatController: ChatControllerImpl, reactionItems: [ReactionItem], correlationId: Int64?) -> (() -> UndoOverlayControllerAdditionalView?)? {
if !chatController.presentationInterfaceState.isPremium {
return nil
}
guard let correlationId else {
return nil
}

View File

@ -334,7 +334,8 @@ private func extractAssociatedData(
recommendedChannels: RecommendedChannels?,
audioTranscriptionTrial: AudioTranscription.TrialState,
chatThemes: [TelegramTheme],
deviceContactsNumbers: Set<String>
deviceContactsNumbers: Set<String>,
isInline: Bool
) -> ChatMessageItemAssociatedData {
var automaticDownloadPeerId: EnginePeer.Id?
var automaticMediaDownloadPeerType: MediaAutoDownloadPeerType = .channel
@ -389,7 +390,7 @@ private func extractAssociatedData(
automaticDownloadPeerId = message.peerId
}
return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers)
return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: isInline)
}
private extension ChatHistoryLocationInput {
@ -1133,6 +1134,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
let selectedMessages = self.selectedMessages
let messageTransitionNode = self.messageTransitionNode
let mode = self.mode
let rotated = self.rotated
var resetScrollingMessageId: (index: MessageIndex, offset: CGFloat)?
@ -1639,7 +1641,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
translateToLanguage = languageCode
}
let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers)
let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: !rotated)
let filteredEntries = chatHistoryEntriesForView(
location: chatLocation,

View File

@ -769,7 +769,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
loadLimits,
loadStickerSaveStatusSignal,
loadResourceStatusSignal,
context.sharedContext.chatAvailableMessageActions(engine: context.engine, accountPeerId: context.account.peerId, messageIds: Set(messages.map { $0.id })),
context.sharedContext.chatAvailableMessageActions(engine: context.engine, accountPeerId: context.account.peerId, messageIds: Set(messages.map { $0.id }), keepUpdated: false),
context.account.pendingUpdateMessageManager.updatingMessageMedia
|> take(1),
infoSummaryData,
@ -810,10 +810,12 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
canSelect: canSelect && !isEmbeddedMode,
resourceStatus: resourceStatus,
messageActions: isEmbeddedMode ? ChatAvailableMessageActions(
options: messageActions.options.intersection([.deleteLocally, .deleteGlobally]),
options: messageActions.options.intersection([.deleteLocally, .deleteGlobally, .forward]),
banAuthor: nil,
disableDelete: true,
isCopyProtected: messageActions.isCopyProtected
isCopyProtected: messageActions.isCopyProtected,
setTag: false,
editTags: Set()
) : messageActions
), updatingMessageMedia, infoSummaryData, appConfig, isMessageRead, messageViewsPrivacyTips, availableReactions, translationSettings, loggingSettings, notificationSoundList, accountPeer)
}
@ -1891,13 +1893,22 @@ private func canPerformDeleteActions(limits: LimitsConfiguration, accountPeerId:
return false
}
func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: PeerId, messageIds: Set<MessageId>, messages: [MessageId: Message] = [:], peers: [PeerId: Peer] = [:]) -> Signal<ChatAvailableMessageActions, NoError> {
return engine.data.get(
func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: PeerId, messageIds: Set<MessageId>, messages: [MessageId: Message] = [:], peers: [PeerId: Peer] = [:], keepUpdated: Bool) -> Signal<ChatAvailableMessageActions, NoError> {
return engine.data.subscribe(
TelegramEngine.EngineData.Item.Configuration.Limits(),
EngineDataMap(Set(messageIds.map(\.peerId)).map(TelegramEngine.EngineData.Item.Peer.Peer.init)),
EngineDataMap(Set(messageIds).map(TelegramEngine.EngineData.Item.Messages.Message.init))
EngineDataMap(Set(messageIds).map(TelegramEngine.EngineData.Item.Messages.Message.init)),
TelegramEngine.EngineData.Item.Peer.Peer(id: accountPeerId)
)
|> map { limitsConfiguration, peerMap, messageMap -> ChatAvailableMessageActions in
|> take(keepUpdated ? Int.max : 1)
|> map { limitsConfiguration, peerMap, messageMap, accountPeer -> ChatAvailableMessageActions in
let isPremium: Bool
if let accountPeer {
isPremium = accountPeer.isPremium
} else {
isPremium = false
}
var optionsMap: [MessageId: ChatAvailableMessageActionOptions] = [:]
var banPeer: Peer?
var hadPersonalIncoming = false
@ -1906,6 +1917,9 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer
var isCopyProtected = false
var isShareProtected = false
var setTag = false
var commonTags: Set<MessageReaction.Reaction>?
func getPeer(_ peerId: PeerId) -> Peer? {
if let maybePeer = peerMap[peerId], let peer = maybePeer {
return peer._asPeer()
@ -1933,6 +1947,25 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer
optionsMap[id] = []
}
if let message = getMessage(id) {
if message.areReactionsTags(accountPeerId: accountPeerId) {
setTag = true
var messageReactions = Set<MessageReaction.Reaction>()
if let reactionsAttribute = mergedMessageReactions(attributes: message.attributes, isTags: message.areReactionsTags(accountPeerId: accountPeerId)) {
for reaction in reactionsAttribute.reactions {
messageReactions.insert(reaction.value)
}
}
if let commonTagsValue = commonTags {
if commonTagsValue == messageReactions {
} else {
commonTags?.removeAll()
}
} else {
commonTags = messageReactions
}
}
if message.isCopyProtected() || message.containsSecretMedia {
isCopyProtected = true
}
@ -2144,9 +2177,15 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer
if hadPersonalIncoming && optionsMap.values.contains(where: { $0.contains(.deleteGlobally) }) && !reducedOptions.contains(.deleteGlobally) {
reducedOptions.insert(.unsendPersonal)
}
return ChatAvailableMessageActions(options: reducedOptions, banAuthor: banPeer, disableDelete: disableDelete, isCopyProtected: isCopyProtected)
if !isPremium {
setTag = false
commonTags = nil
}
return ChatAvailableMessageActions(options: reducedOptions, banAuthor: banPeer, disableDelete: disableDelete, isCopyProtected: isCopyProtected, setTag: setTag, editTags: commonTags ?? Set())
} else {
return ChatAvailableMessageActions(options: [], banAuthor: nil, disableDelete: false, isCopyProtected: isCopyProtected)
return ChatAvailableMessageActions(options: [], banAuthor: nil, disableDelete: false, isCopyProtected: isCopyProtected, setTag: false, editTags: Set())
}
}
}

View File

@ -72,7 +72,15 @@ func rightNavigationButtonForChatInterfaceState(context: AccountContext, present
}
if case let .replyThread(message) = presentationInterfaceState.chatLocation, message.peerId == context.account.peerId {
return chatInfoNavigationButton
let isTags = presentationInterfaceState.hasSearchTags
if case .search(isTags) = currentButton?.action {
return currentButton
} else {
let buttonItem = UIBarButtonItem(image: isTags ? PresentationResourcesRootController.navigationCompactTagsSearchIcon(presentationInterfaceState.theme) : PresentationResourcesRootController.navigationCompactSearchIcon(presentationInterfaceState.theme), style: .plain, target: target, action: selector)
buttonItem.accessibilityLabel = strings.Conversation_Search
return ChatNavigationButton(action: .search(hasTags: isTags), buttonItem: buttonItem)
}
}
if let channel = presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.flags.contains(.isForum), let moreInfoNavigationButton = moreInfoNavigationButton {
@ -144,27 +152,29 @@ func rightNavigationButtonForChatInterfaceState(context: AccountContext, present
if case .standard(.previewing) = presentationInterfaceState.mode {
return chatInfoNavigationButton
} else if let peer = presentationInterfaceState.renderedPeer?.peer {
if presentationInterfaceState.accountPeerId == peer.id {
} else if let peerId = presentationInterfaceState.chatLocation.peerId {
if presentationInterfaceState.accountPeerId == peerId {
var displaySearchButton = false
if case .replyThread = presentationInterfaceState.chatLocation {
displaySearchButton = true
}
if case .scheduledMessages = presentationInterfaceState.subject {
return chatInfoNavigationButton
} else {
displaySearchButton = true
}
if displaySearchButton {
let isTags = presentationInterfaceState.hasSearchTags
if presentationInterfaceState.hasPlentyOfMessages || isTags {
if case .search(isTags) = currentButton?.action {
return currentButton
} else {
let buttonItem = UIBarButtonItem(image: isTags ? PresentationResourcesRootController.navigationCompactTagsSearchIcon(presentationInterfaceState.theme) : PresentationResourcesRootController.navigationCompactSearchIcon(presentationInterfaceState.theme), style: .plain, target: target, action: selector)
buttonItem.accessibilityLabel = strings.Conversation_Search
return ChatNavigationButton(action: .search(hasTags: isTags), buttonItem: buttonItem)
}
if case .search(isTags) = currentButton?.action {
return currentButton
} else {
if case .spacer = currentButton?.action {
return currentButton
} else {
return ChatNavigationButton(action: .spacer, buttonItem: UIBarButtonItem(title: "", style: .plain, target: target, action: selector))
}
let buttonItem = UIBarButtonItem(image: isTags ? PresentationResourcesRootController.navigationCompactTagsSearchIcon(presentationInterfaceState.theme) : PresentationResourcesRootController.navigationCompactSearchIcon(presentationInterfaceState.theme), style: .plain, target: target, action: selector)
buttonItem.accessibilityLabel = strings.Conversation_Search
return ChatNavigationButton(action: .search(hasTags: isTags), buttonItem: buttonItem)
}
}
}

View File

@ -12,13 +12,15 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat
if chatPresentationInterfaceState.renderedPeer?.peer?.restrictionText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) != nil {
return nil
}
if let search = chatPresentationInterfaceState.search, chatPresentationInterfaceState.hasSearchTags {
if let search = chatPresentationInterfaceState.search {
var matches = false
if chatPresentationInterfaceState.chatLocation.peerId == context.account.peerId {
if case .everything = search.domain {
matches = true
} else if case .tag = search.domain, search.query.isEmpty {
matches = true
if chatPresentationInterfaceState.hasSearchTags || !chatPresentationInterfaceState.isPremium {
if case .everything = search.domain {
matches = true
} else if case .tag = search.domain, search.query.isEmpty {
matches = true
}
}
}

View File

@ -14,6 +14,8 @@ import EmojiStatusComponent
import SwiftSignalKit
import ContextUI
import PromptUI
import BundleIconComponent
import SavedTagNameAlertController
final class ChatSearchTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, UIScrollViewDelegate {
private struct Params: Equatable {
@ -60,6 +62,136 @@ final class ChatSearchTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, UISc
}
}
private final class PromoView: UIView {
private let containerButton: HighlightTrackingButton
private let background: UIImageView
private let title = ComponentView<Empty>()
private let text = ComponentView<Empty>()
private let arrowIcon = ComponentView<Empty>()
let action: () -> Void
init(action: @escaping () -> Void) {
self.action = action
self.containerButton = HighlightTrackingButton()
self.background = UIImageView()
if let image = UIImage(bundleImageName: "Chat/Title Panels/SearchTagTab") {
self.background.image = image.stretchableImage(withLeftCapWidth: 8, topCapHeight: 0).withRenderingMode(.alwaysTemplate)
}
super.init(frame: CGRect())
self.containerButton.layer.allowsGroupOpacity = true
self.containerButton.addSubview(self.background)
self.addSubview(self.containerButton)
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
self.containerButton.highligthedChanged = { [weak self] highlighted in
guard let self else {
return
}
if highlighted {
self.containerButton.alpha = 0.7
} else {
Transition.easeInOut(duration: 0.25).setAlpha(view: self.containerButton, alpha: 1.0)
}
}
}
required init?(coder: NSCoder) {
preconditionFailure()
}
@objc private func pressed() {
self.action()
}
func update(theme: PresentationTheme, strings: PresentationStrings, height: CGFloat, transition: Transition) -> CGSize {
//TODO:localize
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "Add tags", font: Font.medium(14.0), textColor: theme.rootController.navigationBar.accentTextColor))
)),
environment: {},
containerSize: CGSize(width: 200.0, height: 100.0)
)
let size = CGSize(width: titleSize.width, height: height)
let titleFrame = CGRect(origin: CGPoint(x: 0.0, y: floor((size.height - titleSize.height) * 0.5)), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.isUserInteractionEnabled = false
self.containerButton.addSubview(titleView)
}
titleView.frame = titleFrame
}
self.background.tintColor = theme.rootController.navigationBar.accentTextColor.withMultipliedAlpha(0.1)
if let image = self.background.image {
let backgroundFrame = CGRect(origin: CGPoint(x: -6.0, y: floorToScreenPixels((size.height - image.size.height) * 0.5)), size: CGSize(width: size.width + 6.0 + 9.0, height: image.size.height))
transition.setFrame(view: self.background, frame: backgroundFrame)
}
var totalSize = size
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "to your Saved Messages", font: Font.regular(14.0), textColor: theme.rootController.navigationBar.secondaryTextColor))
)),
environment: {},
containerSize: CGSize(width: 200.0, height: 100.0)
)
let arrowSize = self.arrowIcon.update(
transition: .immediate,
component: AnyComponent(BundleIconComponent(
name: "Item List/DisclosureArrow",
tintColor: theme.rootController.navigationBar.secondaryTextColor.withMultipliedAlpha(0.6)
)),
environment: {},
containerSize: CGSize(width: 200.0, height: 100.0)
)
let textSpacing: CGFloat = 13.0
let arrowSpacing: CGFloat = -5.0
totalSize.width += textSpacing
let textFrame = CGRect(origin: CGPoint(x: totalSize.width, y: floor((size.height - textSize.height) * 0.5)), size: textSize)
if let textView = self.text.view {
if textView.superview == nil {
textView.isUserInteractionEnabled = false
self.containerButton.addSubview(textView)
}
textView.frame = textFrame
}
totalSize.width += textSize.width
totalSize.width += arrowSpacing
let arrowFrame = CGRect(origin: CGPoint(x: totalSize.width, y: 1.0 + floor((size.height - arrowSize.height) * 0.5)), size: arrowSize)
if let arrowIconView = self.arrowIcon.view {
if arrowIconView.superview == nil {
arrowIconView.isUserInteractionEnabled = false
self.containerButton.addSubview(arrowIconView)
}
arrowIconView.frame = arrowFrame
}
totalSize.width += arrowSize.width
transition.setFrame(view: self.containerButton, frame: CGRect(origin: CGPoint(), size: totalSize))
return totalSize
}
}
private final class ItemView: UIView {
private let context: AccountContext
private let action: () -> Void
@ -187,7 +319,7 @@ final class ChatSearchTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, UISc
let counterSize = self.counter.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: title, font: Font.regular(14.0), textColor: isSelected ? theme.list.itemCheckColors.foregroundColor : theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.6)))
text: .plain(NSAttributedString(string: title, font: Font.regular(11.0), textColor: isSelected ? theme.list.itemCheckColors.foregroundColor : theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.6)))
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
@ -217,7 +349,7 @@ final class ChatSearchTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, UISc
if theme.overallDarkAppearance {
self.background.tintColor = isSelected ? theme.list.itemCheckColors.fillColor : UIColor(white: 1.0, alpha: 0.1)
} else {
self.background.tintColor = isSelected ? theme.list.itemCheckColors.fillColor : theme.list.controlSecondaryColor
self.background.tintColor = isSelected ? theme.list.itemCheckColors.fillColor : theme.rootController.navigationSearchBar.inputFillColor
}
if let image = self.background.image {
let backgroundFrame = CGRect(origin: CGPoint(x: -6.0, y: floorToScreenPixels((size.height - image.size.height) * 0.5)), size: CGSize(width: size.width + 6.0 + 9.0, height: image.size.height))
@ -253,6 +385,7 @@ final class ChatSearchTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, UISc
private var items: [Item] = []
private var itemViews: [MessageReaction.Reaction: ItemView] = [:]
private var promoView: PromoView?
private var itemsDisposable: Disposable?
@ -379,100 +512,145 @@ final class ChatSearchTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, UISc
contentSize.width += containerInsets.left
var validIds: [MessageReaction.Reaction] = []
var isFirst = true
for item in self.items {
if isFirst {
isFirst = false
} else {
contentSize.width += itemSpacing
}
let itemId = item.reaction
validIds.append(itemId)
if !params.interfaceState.isPremium {
let promoView: PromoView
var itemTransition = transition
var animateIn = false
let itemView: ItemView
if let current = self.itemViews[itemId] {
itemView = current
if let current = self.promoView {
promoView = current
} else {
itemTransition = .immediate
animateIn = true
let reaction = item.reaction
itemView = ItemView(context: self.context, action: { [weak self] in
guard let self else {
promoView = PromoView(action: { [weak self] in
guard let self, let interfaceInteraction = self.interfaceInteraction else {
return
}
let tag = ReactionsMessageAttribute.messageTag(reaction: reaction)
self.interfaceInteraction?.updateHistoryFilter({ filter in
var tags: [EngineMessage.CustomTag] = filter?.customTags ?? []
if let index = tags.firstIndex(of: tag) {
tags.remove(at: index)
} else {
tags.append(tag)
}
if tags.isEmpty {
return nil
} else {
return ChatPresentationInterfaceState.HistoryFilter(customTags: tags, isActive: filter?.isActive ?? true)
}
})
if let itemView = self.itemViews[reaction] {
self.scrollView.scrollRectToVisible(itemView.frame.insetBy(dx: -46.0, dy: 0.0), animated: true)
}
}, contextGesture: { [weak self] gesture, sourceNode in
guard let self, let interfaceInteraction = self.interfaceInteraction, let chatController = interfaceInteraction.chatController() else {
gesture.cancel()
return
}
var items: [ContextMenuItem] = []
let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 })
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Edit Title", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, a in
var replaceImpl: ((ViewController) -> Void)?
let controller = self.context.sharedContext.makePremiumDemoController(context: self.context, subject: .uniqueReactions, action: { [weak self] in
guard let self else {
a(.default)
return
}
c.dismiss(completion: { [weak self] in
guard let self else {
return
}
self.openEditTagTitle(reaction: reaction)
})
})))
let controller = ContextController(presentationData: presentationData, source: .extracted(TagContextExtractedContentSource(controller: chatController, sourceNode: sourceNode, keepInPlace: false)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture)
interfaceInteraction.presentGlobalOverlayController(controller, nil)
let controller = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .settings, forceDark: false, dismissed: nil)
replaceImpl?(controller)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
interfaceInteraction.chatController()?.push(controller)
})
self.itemViews[itemId] = itemView
self.scrollView.addSubview(itemView)
self.promoView = promoView
self.scrollView.addSubview(promoView)
}
var isSelected = false
if let historyFilter = params.interfaceState.historyFilter {
if historyFilter.customTags.contains(ReactionsMessageAttribute.messageTag(reaction: item.reaction)) {
isSelected = true
}
}
let itemSize = itemView.update(item: item, isSelected: isSelected, theme: params.interfaceState.theme, height: panelHeight, transition: .immediate)
let itemSize = promoView.update(theme: params.interfaceState.theme, strings: params.interfaceState.strings, height: panelHeight, transition: .immediate)
let itemFrame = CGRect(origin: CGPoint(x: contentSize.width, y: -5.0), size: itemSize)
itemTransition.updatePosition(layer: itemView.layer, position: itemFrame.center)
if animateIn && transition.isAnimated {
itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
transition.animateTransformScale(view: itemView, from: 0.001)
}
itemView.bounds = CGRect(origin: CGPoint(), size: itemFrame.size)
itemTransition.updatePosition(layer: promoView.layer, position: itemFrame.center)
promoView.bounds = CGRect(origin: CGPoint(), size: itemFrame.size)
contentSize.width += itemSize.width
} else {
if let promoView = self.promoView {
self.promoView = nil
promoView.removeFromSuperview()
}
var isFirst = true
for item in self.items {
if isFirst {
isFirst = false
} else {
contentSize.width += itemSpacing
}
let itemId = item.reaction
validIds.append(itemId)
var itemTransition = transition
var animateIn = false
let itemView: ItemView
if let current = self.itemViews[itemId] {
itemView = current
} else {
itemTransition = .immediate
animateIn = true
let reaction = item.reaction
itemView = ItemView(context: self.context, action: { [weak self] in
guard let self else {
return
}
let tag = ReactionsMessageAttribute.messageTag(reaction: reaction)
self.interfaceInteraction?.updateHistoryFilter({ filter in
var tags: [EngineMessage.CustomTag] = filter?.customTags ?? []
if let index = tags.firstIndex(of: tag) {
tags.remove(at: index)
} else {
tags.append(tag)
}
if tags.isEmpty {
return nil
} else {
return ChatPresentationInterfaceState.HistoryFilter(customTags: tags, isActive: filter?.isActive ?? true)
}
})
if let itemView = self.itemViews[reaction] {
self.scrollView.scrollRectToVisible(itemView.frame.insetBy(dx: -46.0, dy: 0.0), animated: true)
}
}, contextGesture: { [weak self] gesture, sourceNode in
guard let self, let interfaceInteraction = self.interfaceInteraction, let chatController = interfaceInteraction.chatController() else {
gesture.cancel()
return
}
var items: [ContextMenuItem] = []
let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 })
//TODO:localize
items.append(.action(ContextMenuActionItem(text: "Edit Title", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/TagEditName"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, a in
guard let self else {
a(.default)
return
}
c.dismiss(completion: { [weak self] in
guard let self, let item = self.items.first(where: { $0.reaction == reaction }) else {
return
}
self.openEditTagTitle(reaction: reaction, hasTitle: item.title != nil)
})
})))
let controller = ContextController(presentationData: presentationData, source: .extracted(TagContextExtractedContentSource(controller: chatController, sourceNode: sourceNode, keepInPlace: false)), items: .single(ContextController.Items(content: .list(items))), recognizer: nil, gesture: gesture)
interfaceInteraction.presentGlobalOverlayController(controller, nil)
})
self.itemViews[itemId] = itemView
self.scrollView.addSubview(itemView)
}
var isSelected = false
if let historyFilter = params.interfaceState.historyFilter {
if historyFilter.customTags.contains(ReactionsMessageAttribute.messageTag(reaction: item.reaction)) {
isSelected = true
}
}
let itemSize = itemView.update(item: item, isSelected: isSelected, theme: params.interfaceState.theme, height: panelHeight, transition: .immediate)
let itemFrame = CGRect(origin: CGPoint(x: contentSize.width, y: -5.0), size: itemSize)
itemTransition.updatePosition(layer: itemView.layer, position: itemFrame.center)
if animateIn && transition.isAnimated {
itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
transition.animateTransformScale(view: itemView, from: 0.001)
}
itemView.bounds = CGRect(origin: CGPoint(), size: itemFrame.size)
contentSize.width += itemSize.width
}
}
var removedIds: [MessageReaction.Reaction] = []
for (id, itemView) in self.itemViews {
@ -504,16 +682,37 @@ final class ChatSearchTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode, UISc
}
}
private func openEditTagTitle(reaction: MessageReaction.Reaction) {
let _ = (self.context.engine.stickers.savedMessageTagData()
private func openEditTagTitle(reaction: MessageReaction.Reaction, hasTitle: Bool) {
//TODO:localize
let optionTitle = hasTitle ? "Edit Name" : "Add Name"
let reactionFile: Signal<TelegramMediaFile?, NoError>
switch reaction {
case .builtin:
reactionFile = self.context.engine.stickers.availableReactions()
|> take(1)
|> map { availableReactions -> TelegramMediaFile? in
return availableReactions?.reactions.first(where: { $0.value == reaction })?.selectAnimation
}
case let .custom(fileId):
reactionFile = self.context.engine.stickers.resolveInlineStickers(fileIds: [fileId])
|> map { files -> TelegramMediaFile? in
return files.values.first
}
}
let _ = (combineLatest(
self.context.engine.stickers.savedMessageTagData(),
reactionFile
)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] savedMessageTags in
guard let self, let savedMessageTags else {
|> deliverOnMainQueue).start(next: { [weak self] savedMessageTags, reactionFile in
guard let self, let reactionFile, let savedMessageTags else {
return
}
//TODO:localize
let promptController = promptController(sharedContext: self.context.sharedContext, updatedPresentationData: nil, text: "Edit Title", value: savedMessageTags.tags.first(where: { $0.reaction == reaction })?.title ?? "", characterLimit: 10, apply: { [weak self] value in
let promptController = savedTagNameAlertController(context: self.context, updatedPresentationData: nil, text: optionTitle, subtext: "You can label your emoji tag with a text name.", value: savedMessageTags.tags.first(where: { $0.reaction == reaction })?.title ?? "", reaction: reaction, file: reactionFile, characterLimit: 10, apply: { [weak self] value in
guard let self else {
return
}

View File

@ -78,7 +78,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
}, openPeerMention: { _, _ in
}, openMessageContextMenu: { _, _, _, _, _, _ in
}, openMessageReactionContextMenu: { _, _, _, _ in
}, updateMessageReaction: { _, _ in
}, updateMessageReaction: { _, _, _ in
}, activateMessagePinch: { _ in
}, openMessageContextActions: { _, _, _, _ in
}, navigateToMessage: { _, _, _ in

View File

@ -1493,12 +1493,12 @@ public final class SharedAccountContextImpl: SharedAccountContext {
openExternalUrlImpl(context: context, urlContext: urlContext, url: url, forceExternal: forceExternal, presentationData: presentationData, navigationController: navigationController, dismissInput: dismissInput)
}
public func chatAvailableMessageActions(engine: TelegramEngine, accountPeerId: EnginePeer.Id, messageIds: Set<EngineMessage.Id>) -> Signal<ChatAvailableMessageActions, NoError> {
return chatAvailableMessageActionsImpl(engine: engine, accountPeerId: accountPeerId, messageIds: messageIds)
public func chatAvailableMessageActions(engine: TelegramEngine, accountPeerId: EnginePeer.Id, messageIds: Set<EngineMessage.Id>, keepUpdated: Bool) -> Signal<ChatAvailableMessageActions, NoError> {
return chatAvailableMessageActionsImpl(engine: engine, accountPeerId: accountPeerId, messageIds: messageIds, keepUpdated: keepUpdated)
}
public func chatAvailableMessageActions(engine: TelegramEngine, accountPeerId: EnginePeer.Id, messageIds: Set<EngineMessage.Id>, messages: [EngineMessage.Id: EngineMessage] = [:], peers: [EnginePeer.Id: EnginePeer] = [:]) -> Signal<ChatAvailableMessageActions, NoError> {
return chatAvailableMessageActionsImpl(engine: engine, accountPeerId: accountPeerId, messageIds: messageIds, messages: messages.mapValues({ $0._asMessage() }), peers: peers.mapValues({ $0._asPeer() }))
return chatAvailableMessageActionsImpl(engine: engine, accountPeerId: accountPeerId, messageIds: messageIds, messages: messages.mapValues({ $0._asMessage() }), peers: peers.mapValues({ $0._asPeer() }), keepUpdated: false)
}
public func navigateToChatController(_ params: NavigateToChatControllerParams) {
@ -1681,7 +1681,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in
return false }, openPeer: { _, _, _, _ in }, openPeerMention: { _, _ in }, openMessageContextMenu: { _, _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in
}, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in
}, updateMessageReaction: { _, _, _ in }, activateMessagePinch: { _ in
}, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _, _ in }, navigateToMessageStandalone: { _ in
}, navigateToThreadMessage: { _, _, _ in
}, tapMessage: { message in

View File

@ -55,7 +55,6 @@ public struct ExperimentalUISettings: Codable, Equatable {
public var crashOnMemoryPressure: Bool
public var dustEffect: Bool
public var callV2: Bool
public var alternativeStoryMedia: Bool
public var allowWebViewInspection: Bool
public static var defaultSettings: ExperimentalUISettings {
@ -90,7 +89,6 @@ public struct ExperimentalUISettings: Codable, Equatable {
crashOnMemoryPressure: false,
dustEffect: false,
callV2: false,
alternativeStoryMedia: false,
allowWebViewInspection: false
)
}
@ -126,7 +124,6 @@ public struct ExperimentalUISettings: Codable, Equatable {
crashOnMemoryPressure: Bool,
dustEffect: Bool,
callV2: Bool,
alternativeStoryMedia: Bool,
allowWebViewInspection: Bool
) {
self.keepChatNavigationStack = keepChatNavigationStack
@ -159,7 +156,6 @@ public struct ExperimentalUISettings: Codable, Equatable {
self.crashOnMemoryPressure = crashOnMemoryPressure
self.dustEffect = dustEffect
self.callV2 = callV2
self.alternativeStoryMedia = alternativeStoryMedia
self.allowWebViewInspection = allowWebViewInspection
}
@ -196,7 +192,6 @@ public struct ExperimentalUISettings: Codable, Equatable {
self.crashOnMemoryPressure = try container.decodeIfPresent(Bool.self, forKey: "crashOnMemoryPressure") ?? false
self.dustEffect = try container.decodeIfPresent(Bool.self, forKey: "dustEffect") ?? false
self.callV2 = try container.decodeIfPresent(Bool.self, forKey: "callV2") ?? false
self.alternativeStoryMedia = try container.decodeIfPresent(Bool.self, forKey: "alternativeStoryMedia") ?? false
self.allowWebViewInspection = try container.decodeIfPresent(Bool.self, forKey: "allowWebViewInspection") ?? false
}
@ -233,7 +228,6 @@ public struct ExperimentalUISettings: Codable, Equatable {
try container.encode(self.crashOnMemoryPressure, forKey: "crashOnMemoryPressure")
try container.encode(self.dustEffect, forKey: "dustEffect")
try container.encode(self.callV2, forKey: "callV2")
try container.encode(self.alternativeStoryMedia, forKey: "alternativeStoryMedia")
try container.encode(self.allowWebViewInspection, forKey: "allowWebViewInspection")
}
}

View File

@ -369,8 +369,8 @@ public struct MediaAutoDownloadSettings: Codable, Equatable {
public var wifi: MediaAutoDownloadConnection
public var downloadInBackground: Bool
public var energyUsageSettings: EnergyUsageSettings
public var highQualityStories: Bool
public static var defaultSettings: MediaAutoDownloadSettings {
let mb: Int64 = 1024 * 1024
@ -397,15 +397,16 @@ public struct MediaAutoDownloadSettings: Codable, Equatable {
stories: MediaAutoDownloadCategory(contacts: true, otherPrivate: true, groups: true, channels: true, sizeLimit: 20 * mb, predownload: false)
)
)
return MediaAutoDownloadSettings(presets: presets, cellular: MediaAutoDownloadConnection(enabled: true, preset: .medium, custom: nil), wifi: MediaAutoDownloadConnection(enabled: true, preset: .high, custom: nil), downloadInBackground: true, energyUsageSettings: EnergyUsageSettings.default)
return MediaAutoDownloadSettings(presets: presets, cellular: MediaAutoDownloadConnection(enabled: true, preset: .medium, custom: nil), wifi: MediaAutoDownloadConnection(enabled: true, preset: .high, custom: nil), downloadInBackground: true, energyUsageSettings: EnergyUsageSettings.default, highQualityStories: false)
}
public init(presets: MediaAutoDownloadPresets, cellular: MediaAutoDownloadConnection, wifi: MediaAutoDownloadConnection, downloadInBackground: Bool, energyUsageSettings: EnergyUsageSettings) {
public init(presets: MediaAutoDownloadPresets, cellular: MediaAutoDownloadConnection, wifi: MediaAutoDownloadConnection, downloadInBackground: Bool, energyUsageSettings: EnergyUsageSettings, highQualityStories: Bool) {
self.presets = presets
self.cellular = cellular
self.wifi = wifi
self.downloadInBackground = downloadInBackground
self.energyUsageSettings = energyUsageSettings
self.highQualityStories = highQualityStories
}
public init(from decoder: Decoder) throws {
@ -419,8 +420,8 @@ public struct MediaAutoDownloadSettings: Codable, Equatable {
self.wifi = (try? container.decodeIfPresent(MediaAutoDownloadConnection.self, forKey: "wifi")) ?? defaultSettings.wifi
self.downloadInBackground = try container.decode(Int32.self, forKey: "downloadInBackground") != 0
self.energyUsageSettings = (try container.decodeIfPresent(EnergyUsageSettings.self, forKey: "energyUsageSettings")) ?? EnergyUsageSettings.default
self.highQualityStories = try container.decodeIfPresent(Bool.self, forKey: "highQualityStories") ?? false
}
public func encode(to encoder: Encoder) throws {
@ -430,14 +431,15 @@ public struct MediaAutoDownloadSettings: Codable, Equatable {
try container.encode(self.wifi, forKey: "wifi")
try container.encode((self.downloadInBackground ? 1 : 0) as Int32, forKey: "downloadInBackground")
try container.encode(self.energyUsageSettings, forKey: "energyUsageSettings")
try container.encode(self.highQualityStories, forKey: "highQualityStories")
}
public func connectionSettings(for networkType: MediaAutoDownloadNetworkType) -> MediaAutoDownloadConnection {
switch networkType {
case .cellular:
return self.cellular
case .wifi:
return self.wifi
case .cellular:
return self.cellular
case .wifi:
return self.wifi
}
}