mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
[WIP] Release changes
This commit is contained in:
parent
ce83d7510f
commit
953e1598f7
@ -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>
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)))
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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! {
|
||||
|
@ -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) }
|
||||
|
@ -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)
|
||||
)
|
||||
|
@ -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": [],
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
))
|
||||
|
@ -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?
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
))
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
))
|
||||
|
@ -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
|
||||
))
|
||||
|
@ -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
|
||||
))
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
))
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
))
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
))
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
],
|
||||
)
|
@ -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
|
||||
}
|
@ -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",
|
||||
],
|
||||
)
|
@ -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)) {
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -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",
|
||||
],
|
||||
)
|
@ -0,0 +1 @@
|
||||
|
@ -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?()
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "IconDownloadLocked.svg",
|
||||
"filename" : "downloadlocked_24.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
|
@ -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 |
@ -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
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "IconEyeLocked.svg",
|
||||
"filename" : "eyelocked_24.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
|
@ -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 |
175
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/EyeLocked.imageset/eyelocked_24.pdf
vendored
Normal file
175
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/EyeLocked.imageset/eyelocked_24.pdf
vendored
Normal 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
|
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/QualityHd.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/QualityHd.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "hd_24.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
165
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/QualityHd.imageset/hd_24.pdf
vendored
Normal file
165
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/QualityHd.imageset/hd_24.pdf
vendored
Normal 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
|
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/QualityHdLocked.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/QualityHdLocked.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "hdlocked_24.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
221
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/QualityHdLocked.imageset/hdlocked_24.pdf
vendored
Normal file
221
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/QualityHdLocked.imageset/hdlocked_24.pdf
vendored
Normal 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
|
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/QualitySd.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/QualitySd.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "sd_24.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
169
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/QualitySd.imageset/sd_24.pdf
vendored
Normal file
169
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/QualitySd.imageset/sd_24.pdf
vendored
Normal 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
|
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/TagEditName.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/TagEditName.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "tagname_24.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
115
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/TagEditName.imageset/tagname_24.pdf
vendored
Normal file
115
submodules/TelegramUI/Images.xcassets/Chat/Context Menu/TagEditName.imageset/tagname_24.pdf
vendored
Normal 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
|
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "tagedit_30.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -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
|
12
submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/TagIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/TagIcon.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "tagadd_30.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
113
submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/TagIcon.imageset/tagadd_30.pdf
vendored
Normal file
113
submodules/TelegramUI/Images.xcassets/Chat/Input/Accessory Panels/TagIcon.imageset/tagadd_30.pdf
vendored
Normal 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
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -921,10 +921,6 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
return true
|
||||
}
|
||||
|
||||
if strongSelf.chatPresentationInterfaceState.search != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user