Cherry-pick media timer and web app improvements

This commit is contained in:
Ilya Laktyushin 2023-09-07 17:45:41 +04:00
parent 2cc863bb04
commit 6e76fa8bec
102 changed files with 3940 additions and 644 deletions

View file

@ -9859,9 +9859,30 @@ Sorry for the inconvenience.";
"WebApp.AllowWriteConfirmation" = "This will allow the bot **%@** to message you on Telegram.";
"AuthSessions.MessageApp" = "You allowed this bot to message you when you opened %@.";
"Notification.BotWriteAllowedMenu" = "You allowed this bot to message you when you added it to your attachment menu.";
"Notification.BotWriteAllowedRequest" = "You allowed this bot to message you in the app.";
"WebApp.SharePhoneTitle" = "Share Phone Number?";
"WebApp.SharePhoneConfirmation" = "**%@** will know your phone number. This can be useful for integration with other services.";
"WebApp.SharePhoneConfirmationUnblock" = "**%@** will know your phone number. This can be useful for integration with other services.\n\nThis will also unblock the bot.";
"SecretImage.ViewOnce.Title" = "Disappearing Photo";
"SecretVideo.ViewOnce.Title" = "Disappearing Video";
"MediaPicker.Timer.Description" = "Choose how long the media will be kept after opening.";
"MediaPicker.Timer.ViewOnce" = "View Once";
"MediaPicker.Timer.Seconds_1" = "%d Second";
"MediaPicker.Timer.Seconds_any" = "%d Seconds";
"MediaPicker.Timer.DoNotDelete" = "Do Not Delete";
"MediaPicker.Timer.Photo.ViewOnceTooltip" = "Photo set to view once.";
"MediaPicker.Timer.Photo.TimerTooltip" = "Photo will be deleted in\n%@ seconds after opening.";
"MediaPicker.Timer.Photo.KeepTooltip" = "Photo will be kept in chat.";
"MediaPicker.Timer.Video.ViewOnceTooltip" = "Video set to view once.";
"MediaPicker.Timer.Video.TimerTooltip" = "Video will be deleted in\n%@ seconds after opening.";
"MediaPicker.Timer.Video.KeepTooltip" = "Video will be kept in chat.";
"Gallery.ViewOncePhotoTooltip" = "This photo can only be viewed once.";
"Gallery.ViewOnceVideoTooltip" = "This video can only be viewed once.";

View file

@ -1025,6 +1025,7 @@ public protocol AccountContext: AnyObject {
var animatedEmojiStickers: [String: [StickerPackItem]] { get }
var isPremium: Bool { get }
var userLimits: EngineConfiguration.UserLimits { get }
func storeSecureIdPassword(password: String)

View file

@ -446,8 +446,9 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
public var sendPressed: ((NSAttributedString?) -> Void)?
public var focusUpdated: ((Bool) -> Void)?
public var heightUpdated: ((Bool) -> Void)?
public var timerUpdated: ((NSNumber?) -> Void)?
public func updateLayoutSize(_ size: CGSize, sideInset: CGFloat, animated: Bool) -> CGFloat {
public func updateLayoutSize(_ size: CGSize, keyboardHeight: CGFloat, sideInset: CGFloat, animated: Bool) -> CGFloat {
guard let presentationInterfaceState = self.presentationInterfaceState else {
return 0.0
}
@ -460,6 +461,16 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
}
}
public func setTimeout(_ timeout: Int32) {
}
public func animate(_ view: UIView, frame: CGRect) {
}
public func onAnimateOut() {
}
public func dismissInput() {
self.ensureUnfocused()
}

View file

@ -19,7 +19,7 @@ public enum AttachmentButtonType: Equatable {
case location
case contact
case poll
case app(Peer, String, [AttachMenuBots.Bot.IconName: TelegramMediaFile])
case app(EnginePeer, String, [AttachMenuBots.Bot.IconName: TelegramMediaFile])
case gift
case standalone
@ -56,7 +56,7 @@ public enum AttachmentButtonType: Equatable {
return false
}
case let .app(lhsPeer, lhsTitle, lhsIcons):
if case let .app(rhsPeer, rhsTitle, rhsIcons) = rhs, arePeersEqual(lhsPeer, rhsPeer), lhsTitle == rhsTitle, lhsIcons == rhsIcons {
if case let .app(rhsPeer, rhsTitle, rhsIcons) = rhs, lhsPeer == rhsPeer, lhsTitle == rhsTitle, lhsIcons == rhsIcons {
return true
} else {
return false
@ -1040,7 +1040,7 @@ public class AttachmentController: ViewController {
|> deliverOnMainQueue).start(next: { bots in
for bot in bots {
for (name, file) in bot.icons {
if [.iOSAnimated, .placeholder].contains(name), let peer = PeerReference(bot.peer) {
if [.iOSAnimated, .placeholder].contains(name), let peer = PeerReference(bot.peer._asPeer()) {
if case .placeholder = name {
let path = context.account.postbox.mediaBox.cachedRepresentationCompletePath(file.resource.id, representation: CachedPreparedSvgRepresentation())
if !FileManager.default.fileExists(atPath: path) {

View file

@ -176,7 +176,7 @@ private final class AttachButtonComponent: CombinedComponent {
let imageName: String
var imageFile: TelegramMediaFile?
var animationFile: TelegramMediaFile?
var botPeer: Peer?
var botPeer: EnginePeer?
let component = context.component
let strings = component.strings
@ -245,7 +245,7 @@ private final class AttachButtonComponent: CombinedComponent {
)
} else {
var fileReference: FileMediaReference?
if let peer = botPeer.flatMap({ PeerReference($0 )}), let imageFile = imageFile {
if let peer = botPeer.flatMap({ PeerReference($0._asPeer())}), let imageFile = imageFile {
fileReference = .attachBot(peer: peer, media: imageFile)
}
@ -1143,7 +1143,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
if case let .app(peer, _, iconFiles) = type {
for (name, file) in iconFiles {
if [.default, .iOSAnimated, .placeholder].contains(name) {
if self.iconDisposables[file.fileId] == nil, let peer = PeerReference(peer) {
if self.iconDisposables[file.fileId] == nil, let peer = PeerReference(peer._asPeer()) {
if case .placeholder = name {
let account = self.context.account
let path = account.postbox.mediaBox.cachedRepresentationCompletePath(file.resource.id, representation: CachedPreparedSvgRepresentation())

View file

@ -264,18 +264,18 @@ private final class VisualMediaItemNode: ASDisplayNode {
if isStreamable {
switch status {
case let .Fetching(_, progress):
let progressString = String(format: "%d%%", Int(progress * 100.0))
badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: progressString))
mediaDownloadState = .compactFetching(progress: 0.0)
case .Local:
badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString))
case .Remote, .Paused:
badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString))
mediaDownloadState = .compactRemote
case let .Fetching(_, progress):
let progressString = String(format: "%d%%", Int(progress * 100.0))
badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: progressString), iconName: nil)
mediaDownloadState = .compactFetching(progress: 0.0)
case .Local:
badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString), iconName: nil)
case .Remote, .Paused:
badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString), iconName: nil)
mediaDownloadState = .compactRemote
}
} else {
badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString))
badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString), iconName: nil)
}
strongSelf.mediaBadgeNode.update(theme: nil, content: badgeContent, mediaDownloadState: mediaDownloadState, alignment: .right, animated: false, badgeAnimated: false)

View file

@ -18,13 +18,13 @@ public enum ChatMessageInteractiveMediaDownloadState: Equatable {
}
public enum ChatMessageInteractiveMediaBadgeContent: Equatable {
case text(inset: CGFloat, backgroundColor: UIColor, foregroundColor: UIColor, text: NSAttributedString)
case text(inset: CGFloat, backgroundColor: UIColor, foregroundColor: UIColor, text: NSAttributedString, iconName: String?)
case mediaDownload(backgroundColor: UIColor, foregroundColor: UIColor, duration: String, size: String?, muted: Bool, active: Bool)
public static func ==(lhs: ChatMessageInteractiveMediaBadgeContent, rhs: ChatMessageInteractiveMediaBadgeContent) -> Bool {
switch lhs {
case let .text(lhsInset, lhsBackgroundColor, lhsForegroundColor, lhsText):
if case let .text(rhsInset, rhsBackgroundColor, rhsForegroundColor, rhsText) = rhs, lhsInset.isEqual(to: rhsInset), lhsBackgroundColor.isEqual(rhsBackgroundColor), lhsForegroundColor.isEqual(rhsForegroundColor), lhsText.isEqual(to: rhsText) {
case let .text(lhsInset, lhsBackgroundColor, lhsForegroundColor, lhsText, lhsIconName):
if case let .text(rhsInset, rhsBackgroundColor, rhsForegroundColor, rhsText, rhsIconName) = rhs, lhsInset.isEqual(to: rhsInset), lhsBackgroundColor.isEqual(rhsBackgroundColor), lhsForegroundColor.isEqual(rhsForegroundColor), lhsText.isEqual(to: rhsText), lhsIconName == rhsIconName {
return true
} else {
return false
@ -48,6 +48,7 @@ public final class ChatMessageInteractiveMediaBadge: ASDisplayNode {
private var previousContentSize: CGSize?
private var backgroundNodeColor: UIColor?
private var foregroundColor: UIColor?
private var iconName: String?
private let backgroundNode: ASImageNode
private let durationNode: ASTextNode
@ -107,14 +108,18 @@ public final class ChatMessageInteractiveMediaBadge: ASDisplayNode {
}
switch content {
case let .text(inset, backgroundColor, foregroundColor, text):
case let .text(inset, backgroundColor, foregroundColor, text, iconName):
transition = .immediate
if self.backgroundNodeColor != backgroundColor {
self.backgroundNodeColor = backgroundColor
self.backgroundNode.image = generateStretchableFilledCircleImage(radius: 9.0, color: backgroundColor)
}
let convertedText = NSMutableAttributedString(string: text.string, attributes: [.font: font, .foregroundColor: foregroundColor])
var textFont = font
if iconName != nil {
textFont = boldFont
}
let convertedText = NSMutableAttributedString(string: text.string, attributes: [.font: textFont, .foregroundColor: foregroundColor])
text.enumerateAttributes(in: NSRange(location: 0, length: text.length), options: []) { attributes, range, _ in
if let _ = attributes[ChatTextInputAttributes.bold] {
convertedText.addAttribute(.font, value: boldFont, range: range)
@ -122,12 +127,33 @@ public final class ChatMessageInteractiveMediaBadge: ASDisplayNode {
}
self.durationNode.attributedText = convertedText
let durationSize = self.durationNode.measure(CGSize(width: 160.0, height: 160.0))
self.durationNode.frame = CGRect(x: 7.0 + inset, y: 3.0, width: durationSize.width, height: durationSize.height)
self.durationNode.frame = CGRect(x: 7.0 + inset, y: 2.0 + UIScreenPixel, width: durationSize.width, height: durationSize.height)
currentContentSize = CGSize(width: widthForString(text.string) + 14.0 + inset, height: 18.0)
if let iconNode = self.iconNode {
transition.updateTransformScale(node: iconNode, scale: 0.001)
transition.updateAlpha(node: iconNode, alpha: 0.0)
if let iconName {
let iconNode: ASImageNode
if let current = self.iconNode {
iconNode = current
} else {
iconNode = ASImageNode()
self.iconNode = iconNode
self.backgroundNode.addSubnode(iconNode)
}
if self.foregroundColor != foregroundColor || self.iconName != iconName {
self.foregroundColor = foregroundColor
self.iconName = iconName
iconNode.image = generateTintedImage(image: UIImage(bundleImageName: iconName), color: foregroundColor)
}
transition.updateAlpha(node: iconNode, alpha: 1.0)
transition.updateTransformScale(node: iconNode, scale: 1.0)
iconNode.frame = CGRect(x: 3.0, y: 2.0, width: 12.0, height: 14.0)
} else {
if let iconNode = self.iconNode {
transition.updateTransformScale(node: iconNode, scale: 0.001)
transition.updateAlpha(node: iconNode, alpha: 0.0)
}
}
case let .mediaDownload(backgroundColor, foregroundColor, duration, size, muted, active):
if self.backgroundNodeColor != backgroundColor {
@ -209,7 +235,7 @@ public final class ChatMessageInteractiveMediaBadge: ASDisplayNode {
let durationWidth = widthForString(duration)
transition.updatePosition(node: iconNode, position: CGPoint(x: (active ? 42.0 : 7.0) + durationWidth + 4.0 + 7.0, y: (active ? 8.0 : 4.0) + 5.0))
if muted {
transition.updateAlpha(node: iconNode, alpha: 1.0)
transition.updateTransformScale(node: iconNode, scale: 1.0)

View file

@ -242,7 +242,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
contentNodes.append(ActionSheetItemNode(theme: self.presentationData.theme, title: self.presentationData.strings.Conversation_SendMessage_SendSilently, icon: .sendWithoutSound, hasSeparator: true, action: {
sendSilently?()
}))
if canSendWhenOnline {
if canSendWhenOnline && schedule != nil {
contentNodes.append(ActionSheetItemNode(theme: self.presentationData.theme, title: self.presentationData.strings.Conversation_SendMessage_SendWhenOnline, icon: .sendWhenOnline, hasSeparator: true, action: {
sendWhenOnline?()
}))

View file

@ -89,7 +89,7 @@ extension UIView {
}
open class ComponentState {
var _updated: ((Transition) -> Void)?
open var _updated: ((Transition) -> Void)?
var isUpdated: Bool = false
public init() {

View file

@ -32,9 +32,34 @@ public final class CounterContollerTitleView: UIView {
}
}
private var primaryTextColor: UIColor?
private var secondaryTextColor: UIColor?
public func updateTextColors(primary: UIColor?, secondary: UIColor?, transition: ContainedViewLayoutTransition) {
self.primaryTextColor = primary
self.secondaryTextColor = secondary
if case let .animated(duration, curve) = transition {
if let snapshotView = self.snapshotContentTree() {
snapshotView.frame = self.bounds
self.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false, completion: { _ in
snapshotView.removeFromSuperview()
})
self.titleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction)
self.subtitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction)
}
}
self.update()
}
private func update() {
self.titleNode.attributedText = NSAttributedString(string: self.title.title, font: Font.semibold(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor)
self.subtitleNode.attributedText = NSAttributedString(string: self.title.counter, font: Font.regular(13.0), textColor: self.theme.rootController.navigationBar.secondaryTextColor)
let primaryTextColor = self.primaryTextColor ?? self.theme.rootController.navigationBar.primaryTextColor
let secondaryTextColor = self.secondaryTextColor ?? self.theme.rootController.navigationBar.secondaryTextColor
self.titleNode.attributedText = NSAttributedString(string: self.title.title, font: Font.semibold(17.0), textColor: primaryTextColor)
self.subtitleNode.attributedText = NSAttributedString(string: self.title.counter, font: Font.regular(13.0), textColor: secondaryTextColor)
self.accessibilityLabel = self.title.title
self.accessibilityValue = self.title.counter

View file

@ -1186,11 +1186,11 @@ open class NavigationBar: ASDisplayNode {
transition.updateAlpha(node: self.stripeNode, alpha: alpha, delay: 0.15)
}
public func updatePresentationData(_ presentationData: NavigationBarPresentationData) {
public func updatePresentationData(_ presentationData: NavigationBarPresentationData, transition: ContainedViewLayoutTransition = .immediate) {
if presentationData.theme !== self.presentationData.theme || presentationData.strings !== self.presentationData.strings {
self.presentationData = presentationData
self.backgroundNode.updateColor(color: self.presentationData.theme.backgroundColor, transition: .immediate)
self.backgroundNode.updateColor(color: self.presentationData.theme.backgroundColor, transition: transition)
self.backButtonNode.color = self.presentationData.theme.buttonColor
self.backButtonNode.disabledColor = self.presentationData.theme.disabledButtonColor

View file

@ -46,6 +46,8 @@ swift_library(
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/TelegramUI/Components/SliderContextItem:SliderContextItem",
"//submodules/TooltipUI",
"//submodules/TelegramNotices",
],
visibility = [
"//visibility:public",

View file

@ -880,6 +880,10 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
displayCaption = !self.textNode.isHidden
}
if metrics.isTablet {
self.fullscreenButton.isHidden = true
}
var textFrame = CGRect()
var visibleTextHeight: CGFloat = 0.0
if !self.textNode.isHidden {

View file

@ -183,6 +183,7 @@ public func galleryItemForEntry(
location: location,
translateToLanguage: translateToLanguage,
peerIsCopyProtected: peerIsCopyProtected,
isSecret: isSecret,
displayInfoOnTop: displayInfoOnTop,
performAction: performAction,
openActionOptions: openActionOptions,
@ -264,6 +265,7 @@ public func galleryItemForEntry(
location: location,
translateToLanguage: translateToLanguage,
peerIsCopyProtected: peerIsCopyProtected,
isSecret: isSecret,
displayInfoOnTop: displayInfoOnTop,
performAction: performAction,
openActionOptions: openActionOptions,

View file

@ -115,18 +115,20 @@ class ChatImageGalleryItem: GalleryItem {
let location: MessageHistoryEntryLocation?
let translateToLanguage: String?
let peerIsCopyProtected: Bool
let isSecret: Bool
let displayInfoOnTop: Bool
let performAction: (GalleryControllerInteractionTapAction) -> Void
let openActionOptions: (GalleryControllerInteractionTapAction, Message) -> Void
let present: (ViewController, Any?) -> Void
init(context: AccountContext, presentationData: PresentationData, message: Message, location: MessageHistoryEntryLocation?, translateToLanguage: String? = nil, peerIsCopyProtected: Bool = false, displayInfoOnTop: Bool, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) {
init(context: AccountContext, presentationData: PresentationData, message: Message, location: MessageHistoryEntryLocation?, translateToLanguage: String? = nil, peerIsCopyProtected: Bool = false, isSecret: Bool = false, displayInfoOnTop: Bool, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction, Message) -> Void, present: @escaping (ViewController, Any?) -> Void) {
self.context = context
self.presentationData = presentationData
self.message = message
self.location = location
self.translateToLanguage = translateToLanguage
self.peerIsCopyProtected = peerIsCopyProtected
self.isSecret = isSecret
self.displayInfoOnTop = displayInfoOnTop
self.performAction = performAction
self.openActionOptions = openActionOptions
@ -136,7 +138,7 @@ class ChatImageGalleryItem: GalleryItem {
func node(synchronous: Bool) -> GalleryItemNode {
let node = ChatImageGalleryItemNode(context: self.context, presentationData: self.presentationData, performAction: self.performAction, openActionOptions: self.openActionOptions, present: self.present)
node.setMessage(self.message, displayInfo: !self.displayInfoOnTop, translateToLanguage: self.translateToLanguage, peerIsCopyProtected: self.peerIsCopyProtected)
node.setMessage(self.message, displayInfo: !self.displayInfoOnTop, translateToLanguage: self.translateToLanguage, peerIsCopyProtected: self.peerIsCopyProtected, isSecret: self.isSecret)
for media in self.message.media {
if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case let .full(fullMedia) = extendedMedia, let image = fullMedia as? TelegramMediaImage {
node.setImage(userLocation: .peer(self.message.id.peerId), imageReference: .message(message: MessageReference(self.message), media: image))
@ -175,7 +177,7 @@ class ChatImageGalleryItem: GalleryItem {
if self.displayInfoOnTop {
node.titleContentView?.setMessage(self.message, presentationData: self.presentationData, accountPeerId: self.context.account.peerId)
}
node.setMessage(self.message, displayInfo: !self.displayInfoOnTop, translateToLanguage: self.translateToLanguage, peerIsCopyProtected: self.peerIsCopyProtected)
node.setMessage(self.message, displayInfo: !self.displayInfoOnTop, translateToLanguage: self.translateToLanguage, peerIsCopyProtected: self.peerIsCopyProtected, isSecret: self.isSecret)
}
}
@ -204,6 +206,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
private var message: Message?
private var translateToLanguage: String?
private var peerIsCopyProtected: Bool = false
private var isSecret: Bool = false
private let presentationData: PresentationData
private let imageNode: TransformImageNode
@ -328,11 +331,12 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(), size: statusSize))
}
fileprivate func setMessage(_ message: Message, displayInfo: Bool, translateToLanguage: String?, peerIsCopyProtected: Bool) {
fileprivate func setMessage(_ message: Message, displayInfo: Bool, translateToLanguage: String?, peerIsCopyProtected: Bool, isSecret: Bool) {
self.message = message
self.translateToLanguage = translateToLanguage
self.peerIsCopyProtected = peerIsCopyProtected
self.imageNode.captureProtected = message.isCopyProtected()
self.isSecret = isSecret
self.imageNode.captureProtected = message.isCopyProtected() || peerIsCopyProtected || isSecret
self.footerContentNode.setMessage(message, displayInfo: displayInfo, translateToLanguage: translateToLanguage, peerIsCopyProtected: peerIsCopyProtected)
}

View file

@ -2771,6 +2771,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
self.playbackRatePromise.set(self.playbackRate ?? 1.0)
}
public func seekToStart() {
self.videoNode?.seek(0.0)
self.videoNode?.play()
}
override var keyShortcuts: [KeyShortcut] {
let strings = self.presentationData.strings

View file

@ -11,6 +11,8 @@ import RadialStatusNode
import ScreenCaptureDetection
import AppBundle
import LocalizedPeerData
import TooltipUI
import TelegramNotices
private func galleryMediaForMedia(media: Media) -> Media? {
if let media = media as? TelegramMediaImage {
@ -54,26 +56,34 @@ private func mediaForMessage(message: Message) -> Media? {
}
private final class SecretMediaPreviewControllerNode: GalleryControllerNode {
private var timeoutNode: RadialStatusNode?
fileprivate var timeoutNode: RadialStatusNode?
private var validLayout: (ContainerViewLayout, CGFloat)?
var beginTimeAndTimeout: (Double, Double)? {
didSet {
if let (beginTime, timeout) = self.beginTimeAndTimeout {
var beginTime = beginTime
if self.timeoutNode == nil {
let timeoutNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5))
self.timeoutNode = timeoutNode
var iconImage: UIImage?
if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/SecretMediaIcon"), color: .white) {
let factor: CGFloat = 0.48
iconImage = generateImage(CGSize(width: floor(image.size.width * factor), height: floor(image.size.height * factor)), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size))
})
let icon: RadialStatusNodeState.SecretTimeoutIcon
let timeoutValue = Int32(timeout)
if timeoutValue == viewOnceTimeout {
beginTime = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
if let image = generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/ViewOnce"), color: .white) {
icon = .image(image)
} else {
icon = .flame
}
} else {
icon = .flame
}
timeoutNode.transitionToState(.secretTimeout(color: .white, icon: iconImage, beginTime: beginTime, timeout: timeout, sparks: true), completion: {})
timeoutNode.transitionToState(.secretTimeout(color: .white, icon: icon, beginTime: beginTime, timeout: timeout, sparks: true), completion: {})
self.addSubnode(timeoutNode)
timeoutNode.addTarget(self, action: #selector(self.statusTapGesture), forControlEvents: .touchUpInside)
if let (layout, navigationHeight) = self.validLayout {
self.layoutTimeoutNode(layout, navigationBarHeight: navigationHeight, transition: .immediate)
@ -86,6 +96,13 @@ private final class SecretMediaPreviewControllerNode: GalleryControllerNode {
}
}
var statusPressed: (UIView) -> Void = { _ in }
@objc private func statusTapGesture() {
if let sourceView = self.timeoutNode?.view {
self.statusPressed(sourceView)
}
}
override func animateIn(animateContent: Bool, useSimpleAnimation: Bool) {
super.animateIn(animateContent: animateContent, useSimpleAnimation: useSimpleAnimation)
@ -139,8 +156,12 @@ public final class SecretMediaPreviewController: ViewController {
private var messageView: MessageView?
private var currentNodeMessageId: MessageId?
private var currentNodeMessageIsVideo = false
private var currentNodeMessageIsViewOnce = false
private var tempFile: TempBoxFile?
private let centralItemAttributesDisposable = DisposableSet();
private let footerContentNode = Promise<(GalleryFooterContentNode?, GalleryOverlayContentNode?)>()
private let _hiddenMedia = Promise<(MessageId, Media)?>(nil)
private var hiddenMediaManagerIndex: Int?
@ -148,6 +169,8 @@ public final class SecretMediaPreviewController: ViewController {
private var screenCaptureEventsDisposable: Disposable?
private weak var tooltipController: TooltipScreen?
public init(context: AccountContext, messageId: MessageId) {
self.context = context
self.messageId = messageId
@ -177,6 +200,15 @@ public final class SecretMediaPreviewController: ViewController {
return nil
}
})
self.centralItemAttributesDisposable.add(self.footerContentNode.get().start(next: { [weak self] footerContentNode, _ in
guard let self else {
return
}
self.controllerNode.updatePresentationState({
$0.withUpdatedFooterContentNode(footerContentNode)
}, transition: .immediate)
}))
}
required public init(coder aDecoder: NSCoder) {
@ -193,6 +225,7 @@ public final class SecretMediaPreviewController: ViewController {
if let tempFile = self.tempFile {
TempBox.shared.dispose(tempFile)
}
self.centralItemAttributesDisposable.dispose()
}
@objc func donePressed() {
@ -213,6 +246,12 @@ public final class SecretMediaPreviewController: ViewController {
self.displayNode = SecretMediaPreviewControllerNode(controllerInteraction: controllerInteraction)
self.displayNodeDidLoad()
self.controllerNode.statusPressed = { [weak self] _ in
if let self {
self.presentViewOnceTooltip()
}
}
self.controllerNode.statusBar = self.statusBar
self.controllerNode.navigationBar = self.navigationBar
@ -231,6 +270,11 @@ public final class SecretMediaPreviewController: ViewController {
self.controllerNode.dismiss = { [weak self] in
self?._hiddenMedia.set(.single(nil))
self?.presentingViewController?.dismiss(animated: false, completion: nil)
if let tooltipController = self?.tooltipController {
self?.tooltipController = nil
tooltipController.dismiss()
}
}
self.controllerNode.beginCustomDismiss = { [weak self] _ in
@ -263,16 +307,20 @@ public final class SecretMediaPreviewController: ViewController {
}
if let attribute = message.autoclearAttribute {
strongSelf.currentNodeMessageIsViewOnce = attribute.timeout == viewOnceTimeout
if let countdownBeginTime = attribute.countdownBeginTime {
if let videoDuration = videoDuration {
if let videoDuration = videoDuration, attribute.timeout != viewOnceTimeout {
beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, videoDuration)
} else {
beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout))
}
}
} else if let attribute = message.autoremoveAttribute {
strongSelf.currentNodeMessageIsViewOnce = attribute.timeout == viewOnceTimeout
if let countdownBeginTime = attribute.countdownBeginTime {
if let videoDuration = videoDuration {
if let videoDuration = videoDuration, attribute.timeout != viewOnceTimeout {
beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, videoDuration)
} else {
beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout))
@ -284,17 +332,29 @@ public final class SecretMediaPreviewController: ViewController {
if file.isAnimated {
strongSelf.title = strongSelf.presentationData.strings.SecretGif_Title
} else {
strongSelf.title = strongSelf.presentationData.strings.SecretVideo_Title
if strongSelf.currentNodeMessageIsViewOnce {
strongSelf.title = strongSelf.presentationData.strings.SecretVideo_ViewOnce_Title
} else {
strongSelf.title = strongSelf.presentationData.strings.SecretVideo_Title
}
}
} else {
strongSelf.title = strongSelf.presentationData.strings.SecretImage_Title
if strongSelf.currentNodeMessageIsViewOnce {
strongSelf.title = strongSelf.presentationData.strings.SecretImage_ViewOnce_Title
} else {
strongSelf.title = strongSelf.presentationData.strings.SecretImage_Title
}
}
if let beginTimeAndTimeout = beginTimeAndTimeout {
strongSelf.controllerNode.beginTimeAndTimeout = beginTimeAndTimeout
}
if !message.flags.contains(.Incoming) {
if strongSelf.currentNodeMessageIsVideo {
if let node = strongSelf.controllerNode.pager.centralItemNode() {
strongSelf.footerContentNode.set(node.footerContent())
}
} else if !message.flags.contains(.Incoming) {
if let _ = beginTimeAndTimeout {
strongSelf.controllerNode.updatePresentationState({
$0.withUpdatedFooterContentNode(nil)
@ -373,9 +433,26 @@ public final class SecretMediaPreviewController: ViewController {
self.controllerNode.animateIn(animateContent: !nodeAnimatesItself, useSimpleAnimation: false)
}
}
if self.currentNodeMessageIsViewOnce {
let _ = (ApplicationSpecificNotice.incrementViewOnceTooltip(accountManager: self.context.sharedContext.accountManager)
|> deliverOnMainQueue).start(next: { [weak self] count in
guard let self else {
return
}
if count < 2 {
self.presentViewOnceTooltip()
}
})
}
}
private func dismiss(forceAway: Bool) {
if let tooltipController = self.tooltipController {
self.tooltipController = nil
tooltipController.dismiss()
}
var animatedOutNode = true
var animatedOutInterface = false
@ -430,7 +507,15 @@ public final class SecretMediaPreviewController: ViewController {
}
guard let item = galleryItemForEntry(context: self.context, presentationData: self.presentationData, entry: MessageHistoryEntry(message: message, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)), streamVideos: false, hideControls: true, isSecret: true, playbackRate: { nil }, tempFilePath: tempFilePath, playbackCompleted: { [weak self] in
self?.dismiss(forceAway: false)
if let self {
if self.currentNodeMessageIsViewOnce {
if let node = self.controllerNode.pager.centralItemNode() as? UniversalVideoGalleryItemNode {
node.seekToStart()
}
} else {
self.dismiss(forceAway: false)
}
}
}, present: { _, _ in }) else {
self._ready.set(.single(true))
return
@ -452,7 +537,7 @@ public final class SecretMediaPreviewController: ViewController {
}
if let attribute = message.autoclearAttribute {
if let countdownBeginTime = attribute.countdownBeginTime {
if let videoDuration = videoDuration {
if let videoDuration = videoDuration, attribute.timeout != viewOnceTimeout {
beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, videoDuration)
} else {
beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout))
@ -460,7 +545,7 @@ public final class SecretMediaPreviewController: ViewController {
}
} else if let attribute = message.autoremoveAttribute {
if let countdownBeginTime = attribute.countdownBeginTime {
if let videoDuration = videoDuration {
if let videoDuration = videoDuration, attribute.timeout != viewOnceTimeout {
beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, videoDuration)
} else {
beginTimeAndTimeout = (Double(countdownBeginTime), Double(attribute.timeout))
@ -478,12 +563,54 @@ public final class SecretMediaPreviewController: ViewController {
if !self.didSetReady {
self._ready.set(.single(true))
}
if !self.currentNodeMessageIsVideo {
if !(self.currentNodeMessageIsVideo || self.currentNodeMessageIsViewOnce) {
self.dismiss()
}
}
}
private func presentViewOnceTooltip() {
guard self.currentNodeMessageIsViewOnce, let sourceView = self.controllerNode.timeoutNode?.view else {
return
}
if let tooltipController = self.tooltipController {
self.tooltipController = nil
tooltipController.dismiss()
}
let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.maxY + 2.0), size: CGSize())
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let iconName = "anim_autoremove_on"
let text: String
if self.currentNodeMessageIsVideo {
text = presentationData.strings.Gallery_ViewOnceVideoTooltip
} else {
text = presentationData.strings.Gallery_ViewOncePhotoTooltip
}
let tooltipController = TooltipScreen(
account: self.context.account,
sharedContext: self.context.sharedContext,
text: .plain(text: text),
balancedTextLayout: true,
constrainWidth: 210.0,
style: .customBlur(UIColor(rgb: 0x18181a), 0.0),
arrowStyle: .small,
icon: .animation(name: iconName, delay: 0.1, tintColor: nil),
location: .point(location, .top),
displayDuration: .default,
inset: 8.0,
shouldDismissOnTouch: { _, _ in
return .ignore
}
)
self.tooltipController = tooltipController
self.present(tooltipController, in: .window(.root))
}
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)

View file

@ -22,18 +22,22 @@
@property (nonatomic, copy) void (^panelFocused)(void);
@property (nonatomic, copy) void (^finishedWithCaption)(NSAttributedString *caption);
@property (nonatomic, copy) void (^keyboardHeightChanged)(CGFloat keyboardHeight, NSTimeInterval duration, NSInteger animationCurve);
@property (nonatomic, copy) void (^timerUpdated)(NSNumber *timeout);
- (void)createInputPanelIfNeeded;
- (void)beginEditing;
- (void)enableDismissal;
- (void)onAnimateOut;
- (void)destroy;
@property (nonatomic, strong) NSAttributedString *caption;
- (void)setCaption:(NSAttributedString *)caption animated:(bool)animated;
- (void)setCaptionPanelHidden:(bool)hidden animated:(bool)animated;
- (void)setTimeout:(int32_t)timeout;
- (void)updateLayoutWithFrame:(CGRect)frame edgeInsets:(UIEdgeInsets)edgeInsets animated:(bool)animated;
@end

View file

@ -22,15 +22,22 @@
@property (nonatomic, readonly) UIView * _Nonnull view;
- (void)setTimeout:(int32_t)timeout;
- (NSAttributedString * _Nonnull)caption;
- (void)setCaption:(NSAttributedString * _Nullable)caption;
- (void)dismissInput;
- (void)animateView:(UIView * _Nonnull)view frame:(CGRect)frame;
- (void)onAnimateOut;
@property (nonatomic, copy) void(^ _Nullable sendPressed)(NSAttributedString * _Nullable string);
@property (nonatomic, copy) void(^ _Nullable focusUpdated)(BOOL focused);
@property (nonatomic, copy) void(^ _Nullable heightUpdated)(BOOL animated);
@property (nonatomic, copy) void(^ _Nullable timerUpdated)(NSNumber * _Nullable value);
- (CGFloat)updateLayoutSize:(CGSize)size sideInset:(CGFloat)sideInset animated:(bool)animated;
- (CGFloat)updateLayoutSize:(CGSize)size keyboardHeight:(CGFloat)keyboardHeight sideInset:(CGFloat)sideInset animated:(bool)animated;
- (CGFloat)baseHeight;
@end

View file

@ -1568,7 +1568,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
}
}
TGMediaPickerSendActionSheetController *controller = [[TGMediaPickerSendActionSheetController alloc] initWithContext:strongSelf->_context isDark:true sendButtonFrame:strongModel.interfaceView.doneButtonFrame canSendSilently:strongSelf->_hasSilentPosting canSendWhenOnline:false canSchedule:effectiveHasSchedule reminder:strongSelf->_reminder hasTimer:strongSelf->_hasTimer];
TGMediaPickerSendActionSheetController *controller = [[TGMediaPickerSendActionSheetController alloc] initWithContext:strongSelf->_context isDark:true sendButtonFrame:strongModel.interfaceView.doneButtonFrame canSendSilently:strongSelf->_hasSilentPosting canSendWhenOnline:effectiveHasSchedule canSchedule:effectiveHasSchedule reminder:strongSelf->_reminder hasTimer:strongSelf->_hasTimer];
controller.send = ^{
__strong TGCameraController *strongSelf = weakSelf;
__strong TGMediaPickerGalleryModel *strongModel = weakModel;

View file

@ -42,6 +42,28 @@
#import <LegacyComponents/TGPhotoCaptionInputMixin.h>
@interface TGMediaPickerGalleryWrapperView: UIView
{
}
@end
@implementation TGMediaPickerGalleryWrapperView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
__block UIView *result = nil;
[self.subviews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(__kindof UIView * _Nonnull view, NSUInteger idx, BOOL * _Nonnull stop) {
UIView *hitTestView = [view hitTest:[self convertPoint:point toView:view] withEvent:event];
if (hitTestView != nil) {
*stop = true;
result = hitTestView;
}
}];
return result;
}
@end
@interface TGMediaPickerGalleryInterfaceView () <ASWatcher>
{
id<TGModernGalleryItem> _currentItem;
@ -121,7 +143,7 @@
_itemHeaderViews = [[NSMutableArray alloc] init];
_itemFooterViews = [[NSMutableArray alloc] init];
_wrapperView = [[UIView alloc] initWithFrame:CGRectZero];
_wrapperView = [[TGMediaPickerGalleryWrapperView alloc] initWithFrame:CGRectZero];
[self addSubview:_wrapperView];
_headerWrapperView = [[UIView alloc] init];
@ -148,6 +170,8 @@
strongSelf->_portraitToolbarView.doneButton.userInteractionEnabled = false;
strongSelf->_landscapeToolbarView.doneButton.userInteractionEnabled = false;
strongSelf->_donePressed(strongSelf->_currentItem);
[strongSelf->_captionMixin onAnimateOut];
};
void(^toolbarDoneLongPressed)(id) = ^(id sender)
{
@ -350,6 +374,18 @@
} completion:nil];
};
_captionMixin.timerUpdated = ^(NSNumber *timeout) {
__strong TGMediaPickerGalleryInterfaceView *strongSelf = weakSelf;
if (strongSelf == nil)
return;
if (![strongSelf->_currentItem conformsToProtocol:@protocol(TGModernGalleryEditableItem)])
return;
id<TGModernGalleryEditableItem> galleryEditableItem = (id<TGModernGalleryEditableItem>)strongSelf->_currentItem;
[strongSelf->_editingContext setTimer:timeout forItem:galleryEditableItem.editableMediaItem];
};
_captionMixin.stickersContext = stickersContext;
[_captionMixin createInputPanelIfNeeded];
@ -800,6 +836,8 @@
id<TGMediaEditAdjustments> adjustments = dict[@"adjustments"];
NSNumber *timer = dict[@"timer"];
[strongSelf->_captionMixin setTimeout:[timer intValue]];
if ([adjustments isKindOfClass:[TGVideoEditAdjustments class]])
{
TGVideoEditAdjustments *videoAdjustments = (TGVideoEditAdjustments *)adjustments;
@ -1285,7 +1323,7 @@
- (void)animateTransitionOutWithDuration:(NSTimeInterval)__unused duration
{
[_captionMixin onAnimateOut];
}
- (void)setTransitionOutProgress:(CGFloat)transitionOutProgress manual:(bool)manual

View file

@ -165,7 +165,7 @@
}
}
TGMediaPickerSendActionSheetController *controller = [[TGMediaPickerSendActionSheetController alloc] initWithContext:strongSelf->_context isDark:true sendButtonFrame:strongSelf.galleryModel.interfaceView.doneButtonFrame canSendSilently:hasSilentPosting canSendWhenOnline:true canSchedule:effectiveHasSchedule reminder:reminder hasTimer:hasTimer];
TGMediaPickerSendActionSheetController *controller = [[TGMediaPickerSendActionSheetController alloc] initWithContext:strongSelf->_context isDark:true sendButtonFrame:strongSelf.galleryModel.interfaceView.doneButtonFrame canSendSilently:hasSilentPosting canSendWhenOnline:effectiveHasSchedule canSchedule:effectiveHasSchedule reminder:reminder hasTimer:false];
controller.send = ^{
__strong TGMediaPickerModernGalleryMixin *strongSelf = weakSelf;
if (strongSelf == nil)

View file

@ -87,6 +87,14 @@
[strongSelf updateLayoutWithFrame:strongSelf->_currentFrame edgeInsets:strongSelf->_currentEdgeInsets animated:animated];
};
_inputPanel.timerUpdated = ^(NSNumber *value) {
__strong TGPhotoCaptionInputMixin *strongSelf = weakSelf;
if (strongSelf.timerUpdated != nil) {
strongSelf.timerUpdated(value);
}
};
_inputPanelView = inputPanel.view;
_backgroundView = [[UIView alloc] init];
@ -95,6 +103,10 @@
[parentView addSubview:_inputPanelView];
}
- (void)onAnimateOut {
[_inputPanel onAnimateOut];
}
- (void)destroy
{
[_inputPanelView removeFromSuperview];
@ -129,6 +141,10 @@
[_inputPanel setCaption:caption];
}
- (void)setTimeout:(int32_t)timeout {
[_inputPanel setTimeout:timeout];
}
- (void)setCaptionPanelHidden:(bool)hidden animated:(bool)__unused animated
{
_inputPanelView.hidden = hidden;
@ -204,7 +220,7 @@
CGRect frame = _currentFrame;
UIEdgeInsets edgeInsets = _currentEdgeInsets;
CGFloat panelHeight = [_inputPanel updateLayoutSize:frame.size sideInset:0.0 animated:false];
CGFloat panelHeight = [_inputPanel updateLayoutSize:frame.size keyboardHeight:keyboardHeight sideInset:0.0 animated:false];
[UIView animateWithDuration:duration delay:0.0f options:(curve << 16) animations:^{
_inputPanelView.frame = CGRectMake(edgeInsets.left, frame.size.height - panelHeight - MAX(edgeInsets.bottom, _keyboardHeight), frame.size.width, panelHeight);
@ -224,7 +240,7 @@
_currentFrame = frame;
_currentEdgeInsets = edgeInsets;
CGFloat panelHeight = [_inputPanel updateLayoutSize:frame.size sideInset:0.0 animated:animated];
CGFloat panelHeight = [_inputPanel updateLayoutSize:frame.size keyboardHeight:_keyboardHeight sideInset:0.0 animated:animated];
CGFloat y = 0.0;
if (frame.size.width > frame.size.height && !TGIsPad()) {
@ -238,14 +254,15 @@
backgroundHeight += _keyboardHeight - edgeInsets.bottom;
}
CGRect panelFrame = CGRectMake(edgeInsets.left, y, frame.size.width, panelHeight);
CGRect backgroundFrame = CGRectMake(edgeInsets.left, y, frame.size.width, backgroundHeight);
if (animated) {
[UIView animateWithDuration:0.2 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
_inputPanelView.frame = CGRectMake(edgeInsets.left, y, frame.size.width, panelHeight);
_backgroundView.frame = CGRectMake(edgeInsets.left, y, frame.size.width, backgroundHeight);
} completion:nil];
[_inputPanel animateView:_inputPanelView frame:panelFrame];
[_inputPanel animateView:_backgroundView frame:backgroundFrame];
} else {
_inputPanelView.frame = CGRectMake(edgeInsets.left, y, frame.size.width, panelHeight);
_backgroundView.frame = CGRectMake(edgeInsets.left, y, frame.size.width, backgroundHeight);
_inputPanelView.frame = panelFrame;
_backgroundView.frame = backgroundFrame;
}
}

View file

@ -54,7 +54,7 @@ public enum LegacyICloudFilePickerMode {
}
}
public func legacyICloudFilePicker(theme: PresentationTheme, mode: LegacyICloudFilePickerMode = .default, documentTypes: [String] = ["public.item"], completion: @escaping ([URL]) -> Void) -> ViewController {
public func legacyICloudFilePicker(theme: PresentationTheme, mode: LegacyICloudFilePickerMode = .default, documentTypes: [String] = ["public.item"], forceDarkTheme: Bool = false, dismissed: @escaping () -> Void = {}, completion: @escaping ([URL]) -> Void) -> ViewController {
var dismissImpl: (() -> Void)?
let legacyController = LegacyICloudFileController(presentation: .modal(animateIn: true), theme: theme, completion: { urls in
dismissImpl?()
@ -86,6 +86,7 @@ public func legacyICloudFilePicker(theme: PresentationTheme, mode: LegacyICloudF
if let legacyController = legacyController {
legacyController.dismiss()
}
dismissed()
}
legacyController.bind(controller: UIViewController())
return legacyController

View file

@ -459,7 +459,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A
}
let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: imageFlags)
if let timer = item.timer, timer > 0 && timer <= 60 {
if let timer = item.timer, timer > 0 && (timer <= 60 || timer == viewOnceTimeout) {
attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil))
}
if let spoiler = item.spoiler, spoiler {
@ -504,7 +504,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A
let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])
var attributes: [MessageAttribute] = []
if let timer = item.timer, timer > 0 && timer <= 60 {
if let timer = item.timer, timer > 0 && (timer <= 60 || timer == viewOnceTimeout) {
attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil))
}
if let spoiler = item.spoiler, spoiler {
@ -749,7 +749,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A
let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: fileAttributes)
if let timer = item.timer, timer > 0 && timer <= 60 {
if let timer = item.timer, timer > 0 && (timer <= 60 || timer == viewOnceTimeout) {
attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil))
}
if let spoiler = item.spoiler, spoiler {

View file

@ -412,6 +412,7 @@ private final class LocationPickerContext: AttachmentMediaPickerContext {
public func storyLocationPickerController(
context: AccountContext,
location: CLLocationCoordinate2D?,
dismissed: @escaping () -> Void,
completion: @escaping (TelegramMediaMap, Int64?, String?, String?, String?) -> Void
) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme)
@ -427,5 +428,8 @@ public func storyLocationPickerController(
}
controller.navigationPresentation = .flatModal
controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
controller.didDismiss = {
dismissed()
}
return controller
}

View file

@ -229,6 +229,9 @@ func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?,
var effectiveHasSchedule = hasSchedule
if let editingContext = editingContext {
if let timer = editingContext.timer(for: item.asset)?.intValue, timer > 0 {
effectiveHasSchedule = false
}
for item in selectionContext.selectedItems() {
if let editableItem = item as? TGMediaEditableItem, let timer = editingContext.timer(for: editableItem)?.intValue, timer > 0 {
effectiveHasSchedule = false
@ -239,6 +242,9 @@ func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?,
let sendWhenOnlineAvailable: Signal<Bool, NoError>
if let peer {
if case .secretChat = peer {
effectiveHasSchedule = false
}
sendWhenOnlineAvailable = context.account.viewTracker.peerView(peer.id)
|> take(1)
|> map { peerView -> Bool in
@ -265,7 +271,7 @@ func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?,
|> take(1)
|> deliverOnMainQueue).start(next: { sendWhenOnlineAvailable in
let legacySheetController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil)
let sheetController = TGMediaPickerSendActionSheetController(context: legacyController.context, isDark: true, sendButtonFrame: model.interfaceView.doneButtonFrame, canSendSilently: hasSilentPosting, canSendWhenOnline: sendWhenOnlineAvailable, canSchedule: effectiveHasSchedule, reminder: reminder, hasTimer: hasTimer)
let sheetController = TGMediaPickerSendActionSheetController(context: legacyController.context, isDark: true, sendButtonFrame: model.interfaceView.doneButtonFrame, canSendSilently: hasSilentPosting, canSendWhenOnline: sendWhenOnlineAvailable && effectiveHasSchedule, canSchedule: effectiveHasSchedule, reminder: reminder, hasTimer: false)
let dismissImpl = { [weak model] in
model?.dismiss(true, false)
dismissAll()

View file

@ -455,11 +455,11 @@ final class MediaPickerGridItemNode: GridItemNode {
targetSize = CGSize(width: 128.0 * scale, height: 128.0 * scale)
}
let assetImageSignal = assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .fastFormat, synchronous: true)
|> then(
assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .highQualityFormat, synchronous: false)
|> delay(0.03, queue: Queue.concurrentDefaultQueue())
)
let assetImageSignal = assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .opportunistic, synchronous: false)
// |> then(
// assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .highQualityFormat, synchronous: false)
// |> delay(0.03, queue: Queue.concurrentDefaultQueue())
// )
if stories {
self.imageNode.contentUpdated = { [weak self] image in

View file

@ -676,68 +676,70 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
if !controller.didSetupGroups {
controller.didSetupGroups = true
controller.groupsPromise.set(
combineLatest(
self.mediaAssetsContext.fetchAssetsCollections(.album),
self.mediaAssetsContext.fetchAssetsCollections(.smartAlbum)
)
|> map { albums, smartAlbums -> [MediaGroupItem] in
var collections: [PHAssetCollection] = []
smartAlbums.enumerateObjects { collection, _, _ in
if [.smartAlbumUserLibrary, .smartAlbumFavorites].contains(collection.assetCollectionSubtype) {
collections.append(collection)
Queue.concurrentDefaultQueue().after(0.3) {
controller.groupsPromise.set(
combineLatest(
self.mediaAssetsContext.fetchAssetsCollections(.album),
self.mediaAssetsContext.fetchAssetsCollections(.smartAlbum)
)
|> map { albums, smartAlbums -> [MediaGroupItem] in
var collections: [PHAssetCollection] = []
smartAlbums.enumerateObjects { collection, _, _ in
if [.smartAlbumUserLibrary, .smartAlbumFavorites].contains(collection.assetCollectionSubtype) {
collections.append(collection)
}
}
}
smartAlbums.enumerateObjects { collection, index, _ in
var supportedAlbums: [PHAssetCollectionSubtype] = [
.smartAlbumBursts,
.smartAlbumPanoramas,
.smartAlbumScreenshots,
.smartAlbumSelfPortraits,
.smartAlbumSlomoVideos,
.smartAlbumTimelapses,
.smartAlbumVideos,
.smartAlbumAllHidden
]
if #available(iOS 11, *) {
supportedAlbums.append(.smartAlbumAnimated)
supportedAlbums.append(.smartAlbumDepthEffect)
supportedAlbums.append(.smartAlbumLivePhotos)
smartAlbums.enumerateObjects { collection, index, _ in
var supportedAlbums: [PHAssetCollectionSubtype] = [
.smartAlbumBursts,
.smartAlbumPanoramas,
.smartAlbumScreenshots,
.smartAlbumSelfPortraits,
.smartAlbumSlomoVideos,
.smartAlbumTimelapses,
.smartAlbumVideos,
.smartAlbumAllHidden
]
if #available(iOS 11, *) {
supportedAlbums.append(.smartAlbumAnimated)
supportedAlbums.append(.smartAlbumDepthEffect)
supportedAlbums.append(.smartAlbumLivePhotos)
}
if supportedAlbums.contains(collection.assetCollectionSubtype) {
let result = PHAsset.fetchAssets(in: collection, options: nil)
if result.count > 0 {
collections.append(collection)
}
}
}
if supportedAlbums.contains(collection.assetCollectionSubtype) {
albums.enumerateObjects(options: [.reverse]) { collection, _, _ in
let result = PHAsset.fetchAssets(in: collection, options: nil)
if result.count > 0 {
collections.append(collection)
}
}
}
albums.enumerateObjects(options: [.reverse]) { collection, _, _ in
let result = PHAsset.fetchAssets(in: collection, options: nil)
if result.count > 0 {
collections.append(collection)
}
}
var items: [MediaGroupItem] = []
for collection in collections {
let result = PHAsset.fetchAssets(in: collection, options: nil)
let firstItem: PHAsset?
if [.smartAlbumUserLibrary, .smartAlbumFavorites].contains(collection.assetCollectionSubtype) {
firstItem = result.lastObject
} else {
firstItem = result.firstObject
}
items.append(
MediaGroupItem(
collection: collection,
firstItem: firstItem,
count: result.count
var items: [MediaGroupItem] = []
for collection in collections {
let result = PHAsset.fetchAssets(in: collection, options: nil)
let firstItem: PHAsset?
if [.smartAlbumUserLibrary, .smartAlbumFavorites].contains(collection.assetCollectionSubtype) {
firstItem = result.lastObject
} else {
firstItem = result.firstObject
}
items.append(
MediaGroupItem(
collection: collection,
firstItem: firstItem,
count: result.count
)
)
)
}
return items
}
return items
}
)
)
}
}
} else if case .notDetermined = mediaAccess, !self.requestedMediaAccess {
self.requestedMediaAccess = true

View file

@ -74,12 +74,36 @@ public final class MoreButtonNode: ASDisplayNode {
private let buttonNode: HighlightableButtonNode
public let iconNode: MoreIconNode
private var color: UIColor?
public var theme: PresentationTheme {
didSet {
self.iconNode.customColor = self.theme.rootController.navigationBar.buttonColor
self.update()
}
}
public func updateColor(_ color: UIColor?, transition: ContainedViewLayoutTransition) {
self.color = color
if case let .animated(duration, curve) = transition {
if let snapshotView = self.iconNode.view.snapshotContentTree() {
snapshotView.frame = self.iconNode.frame
self.view.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false, completion: { _ in
snapshotView.removeFromSuperview()
})
self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction)
}
}
self.update()
}
private func update() {
let color = self.color ?? self.theme.rootController.navigationBar.buttonColor
self.iconNode.customColor = color
}
public init(theme: PresentationTheme) {
self.theme = theme

View file

@ -14,6 +14,7 @@ swift_library(
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/LegacyComponents:LegacyComponents",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/ManagedAnimationNode:ManagedAnimationNode"
],
visibility = [
"//visibility:public",

View file

@ -5,6 +5,7 @@ import AsyncDisplayKit
enum RadialStatusIcon {
case custom(UIImage)
case timeout
case play(UIColor)
case pause(UIColor)
}
@ -22,14 +23,28 @@ private final class RadialStatusIconContentNodeParameters: NSObject {
final class RadialStatusIconContentNode: RadialStatusContentNode {
private let icon: RadialStatusIcon
private var animationNode: FireIconNode?
init(icon: RadialStatusIcon, synchronous: Bool) {
self.icon = icon
super.init()
self.displaysAsynchronously = !synchronous
self.isLayerBacked = true
// self.isLayerBacked = true
self.isOpaque = false
if case .timeout = icon {
let animationNode = FireIconNode()
self.animationNode = animationNode
self.addSubnode(animationNode)
}
}
override func layout() {
super.layout()
self.animationNode?.frame = CGRect(x: 6.0, y: 2.0, width: 36.0, height: 36.0)
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
@ -48,6 +63,8 @@ final class RadialStatusIconContentNode: RadialStatusContentNode {
if let parameters = parameters as? RadialStatusIconContentNodeParameters {
let diameter = min(bounds.size.width, bounds.size.height)
switch parameters.icon {
case .timeout:
break
case let .play(color):
context.setFillColor(color.cgColor)

View file

@ -4,6 +4,35 @@ import AsyncDisplayKit
import Display
public enum RadialStatusNodeState: Equatable {
public enum SecretTimeoutIcon: Equatable {
case none
case image(UIImage)
case flame
public static func ==(lhs: SecretTimeoutIcon, rhs: SecretTimeoutIcon) -> Bool {
switch lhs {
case .none:
if case .none = rhs {
return true
} else {
return false
}
case let .image(lhsImage):
if case let .image(rhsImage) = rhs, lhsImage === rhsImage {
return true
} else {
return false
}
case .flame:
if case .flame = rhs {
return true
} else {
return false
}
}
}
}
case none
case download(UIColor)
case play(UIColor)
@ -12,7 +41,8 @@ public enum RadialStatusNodeState: Equatable {
case cloudProgress(color: UIColor, strokeBackgroundColor: UIColor, lineWidth: CGFloat, value: CGFloat?)
case check(UIColor)
case customIcon(UIImage)
case secretTimeout(color: UIColor, icon: UIImage?, beginTime: Double, timeout: Double, sparks: Bool)
case staticTimeout
case secretTimeout(color: UIColor, icon: SecretTimeoutIcon, beginTime: Double, timeout: Double, sparks: Bool)
public static func ==(lhs: RadialStatusNodeState, rhs: RadialStatusNodeState) -> Bool {
switch lhs {
@ -64,8 +94,14 @@ public enum RadialStatusNodeState: Equatable {
} else {
return false
}
case .staticTimeout:
if case .staticTimeout = rhs {
return true
} else {
return false
}
case let .secretTimeout(lhsColor, lhsIcon, lhsBeginTime, lhsTimeout, lhsSparks):
if case let .secretTimeout(rhsColor, rhsIcon, rhsBeginTime, rhsTimeout, rhsSparks) = rhs, lhsColor.isEqual(rhsColor), lhsIcon === rhsIcon, lhsBeginTime.isEqual(to: rhsBeginTime), lhsTimeout.isEqual(to: rhsTimeout), lhsSparks == rhsSparks {
if case let .secretTimeout(rhsColor, rhsIcon, rhsBeginTime, rhsTimeout, rhsSparks) = rhs, lhsColor.isEqual(rhsColor), lhsIcon == rhsIcon, lhsBeginTime.isEqual(to: rhsBeginTime), lhsTimeout.isEqual(to: rhsTimeout), lhsSparks == rhsSparks {
return true
} else {
return false
@ -123,8 +159,14 @@ public enum RadialStatusNodeState: Equatable {
} else {
return false
}
case .staticTimeout:
if case .staticTimeout = rhs{
return true
} else {
return false
}
case let .secretTimeout(lhsColor, lhsIcon, lhsBeginTime, lhsTimeout, lhsSparks):
if case let .secretTimeout(rhsColor, rhsIcon, rhsBeginTime, rhsTimeout, rhsSparks) = rhs, lhsColor.isEqual(rhsColor), lhsIcon === rhsIcon, lhsBeginTime.isEqual(to: rhsBeginTime), lhsTimeout.isEqual(to: rhsTimeout), lhsSparks == rhsSparks {
if case let .secretTimeout(rhsColor, rhsIcon, rhsBeginTime, rhsTimeout, rhsSparks) = rhs, lhsColor.isEqual(rhsColor), lhsIcon == rhsIcon, lhsBeginTime.isEqual(to: rhsBeginTime), lhsTimeout.isEqual(to: rhsTimeout), lhsSparks == rhsSparks {
return true
} else {
return false
@ -179,6 +221,8 @@ public enum RadialStatusNodeState: Equatable {
node.progress = value
return node
}
case .staticTimeout:
return RadialStatusIconContentNode(icon: .timeout, synchronous: synchronous)
case let .secretTimeout(color, icon, beginTime, timeout, sparks):
return RadialStatusSecretTimeoutContentNode(color: color, beginTime: beginTime, timeout: timeout, icon: icon, sparks: sparks)
}
@ -188,7 +232,9 @@ public enum RadialStatusNodeState: Equatable {
public final class RadialStatusNode: ASControlNode {
public var backgroundNodeColor: UIColor {
didSet {
self.transitionToBackgroundColor(self.state.backgroundColor(color: self.backgroundNodeColor), previousContentNode: nil, animated: false, synchronous: false, completion: {})
if self.backgroundNodeColor != oldValue {
self.transitionToBackgroundColor(self.state.backgroundColor(color: self.backgroundNodeColor), previousContentNode: nil, animated: false, synchronous: false, completion: {})
}
}
}

View file

@ -3,6 +3,7 @@ import UIKit
import Display
import AsyncDisplayKit
import LegacyComponents
import ManagedAnimationNode
private struct ContentParticle {
var position: CGPoint
@ -24,12 +25,12 @@ private struct ContentParticle {
private final class RadialStatusSecretTimeoutContentNodeParameters: NSObject {
let color: UIColor
let icon: UIImage?
let icon: RadialStatusNodeState.SecretTimeoutIcon
let progress: CGFloat
let sparks: Bool
let particles: [ContentParticle]
init(color: UIColor, icon: UIImage?, progress: CGFloat, sparks: Bool, particles: [ContentParticle]) {
init(color: UIColor, icon: RadialStatusNodeState.SecretTimeoutIcon, progress: CGFloat, sparks: Bool, particles: [ContentParticle]) {
self.color = color
self.icon = icon
self.progress = progress
@ -47,15 +48,17 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode {
private let beginTime: Double
private let timeout: Double
private let icon: UIImage?
private let icon: RadialStatusNodeState.SecretTimeoutIcon
private let sparks: Bool
private var progress: CGFloat = 0.0
private var particles: [ContentParticle] = []
private let animationNode = FireIconNode()
private var displayLink: CADisplayLink?
init(color: UIColor, beginTime: Double, timeout: Double, icon: UIImage?, sparks: Bool) {
init(color: UIColor, beginTime: Double, timeout: Double, icon: RadialStatusNodeState.SecretTimeoutIcon, sparks: Bool) {
self.color = color
self.beginTime = beginTime
self.timeout = timeout
@ -65,7 +68,6 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode {
super.init()
self.isOpaque = false
self.isLayerBacked = true
class DisplayLinkProxy: NSObject {
weak var target: RadialStatusSecretTimeoutContentNode?
@ -81,6 +83,10 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode {
self.displayLink = CADisplayLink(target: DisplayLinkProxy(target: self), selector: #selector(DisplayLinkProxy.displayLinkEvent))
self.displayLink?.isPaused = true
self.displayLink?.add(to: RunLoop.main, forMode: .common)
if case .flame = icon {
self.addSubnode(self.animationNode)
}
}
deinit {
@ -89,6 +95,15 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode {
override func layout() {
super.layout()
var factor: CGFloat = 0.75
var offset: CGFloat = 0.0415
if self.bounds.width < 30.0 {
factor = 0.66
offset = 0.08
}
let size = floorToScreenPixels(self.bounds.width * factor)
self.animationNode.frame = CGRect(x: floorToScreenPixels((self.bounds.width - size) / 2.0), y: ceil(self.bounds.height * offset), width: size, height: size)
}
override func animateOut(to: RadialStatusNodeState, completion: @escaping () -> Void) {
@ -118,7 +133,11 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode {
}
let absoluteTimestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970
self.progress = min(1.0, CGFloat((absoluteTimestamp - self.beginTime) / self.timeout))
var progress = min(1.0, CGFloat((absoluteTimestamp - self.beginTime) / self.timeout))
if self.timeout == 0x7fffffff {
progress = 0.0
}
self.progress = progress
if self.sparks {
let lineWidth: CGFloat = 1.75
@ -187,7 +206,8 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode {
}
if let parameters = parameters as? RadialStatusSecretTimeoutContentNodeParameters {
if let icon = parameters.icon, let iconImage = icon.cgImage {
var drawArc = true
if case let .image(icon) = parameters.icon, let iconImage = icon.cgImage {
let imageRect = CGRect(origin: CGPoint(x: floor((bounds.size.width - icon.size.width) / 2.0), y: floor((bounds.size.height - icon.size.height) / 2.0)), size: icon.size)
context.saveGState()
context.translateBy(x: imageRect.midX, y: imageRect.midY)
@ -195,6 +215,8 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode {
context.translateBy(x: -imageRect.midX, y: -imageRect.midY)
context.draw(iconImage, in: imageRect)
context.restoreGState()
drawArc = false
}
let lineWidth: CGFloat
@ -217,10 +239,12 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode {
let startAngle: CGFloat = -CGFloat.pi / 2.0
let endAngle: CGFloat = -CGFloat.pi / 2.0 + 2.0 * CGFloat.pi * parameters.progress
let path = CGMutablePath()
path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
context.addPath(path)
context.strokePath()
if drawArc {
let path = CGMutablePath()
path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
context.addPath(path)
context.strokePath()
}
for particle in parameters.particles {
let size: CGFloat = 1.3
@ -231,3 +255,10 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode {
}
}
final class FireIconNode: ManagedAnimationNode {
init() {
super.init(size: CGSize(width: 100.0, height: 100.0))
self.trackTo(item: ManagedAnimationItem(source: .local("anim_autoremove_on"), frames: .range(startFrame: 0, endFrame: 120), duration: 2.0))
}
}

View file

@ -95,7 +95,7 @@ enum AccountStateMutationOperation {
case UpdatePinnedItemIds(PeerGroupId, AccountStateUpdatePinnedItemIdsOperation)
case UpdatePinnedTopic(peerId: PeerId, threadId: Int64, isPinned: Bool)
case UpdatePinnedTopicOrder(peerId: PeerId, threadIds: [Int64])
case ReadMessageContents((PeerId?, [Int32]))
case ReadMessageContents(peerIdsAndMessageIds: (PeerId?, [Int32]), date: Int32?)
case UpdateMessageImpressionCount(MessageId, Int32)
case UpdateMessageForwardsCount(MessageId, Int32)
case UpdateInstalledStickerPacks(AccountStateUpdateStickerPacksOperation)
@ -574,8 +574,8 @@ struct AccountMutableState {
self.addOperation(.UpdatePinnedTopicOrder(peerId: peerId, threadIds: threadIds))
}
mutating func addReadMessagesContents(_ peerIdsAndMessageIds: (PeerId?, [Int32])) {
self.addOperation(.ReadMessageContents(peerIdsAndMessageIds))
mutating func addReadMessagesContents(_ peerIdsAndMessageIds: (PeerId?, [Int32]), date: Int32?) {
self.addOperation(.ReadMessageContents(peerIdsAndMessageIds: peerIdsAndMessageIds, date: date))
}
mutating func addUpdateMessageImpressionCount(id: MessageId, count: Int32) {

View file

@ -9,11 +9,11 @@ public func tagsForStoreMessage(incoming: Bool, attributes: [MessageAttribute],
var hasUnseenReactions = false
for attribute in attributes {
if let timerAttribute = attribute as? AutoclearTimeoutMessageAttribute {
if timerAttribute.timeout > 0 && timerAttribute.timeout <= 60 {
if timerAttribute.timeout > 0 && (timerAttribute.timeout <= 60 || timerAttribute.timeout == viewOnceTimeout) {
isSecret = true
}
} else if let timerAttribute = attribute as? AutoremoveTimeoutMessageAttribute {
if timerAttribute.timeout > 0 && timerAttribute.timeout <= 60 {
if timerAttribute.timeout > 0 && (timerAttribute.timeout <= 60 || timerAttribute.timeout == viewOnceTimeout) {
isSecret = true
}
} else if let mentionAttribute = attribute as? ConsumablePersonalMentionMessageAttribute {

View file

@ -1479,11 +1479,11 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox:
case let .updateChannelPinnedTopic(flags, channelId, topicId):
let isPinned = (flags & (1 << 0)) != 0
updatedState.addUpdatePinnedTopic(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), threadId: Int64(topicId), isPinned: isPinned)
case let .updateReadMessagesContents(_, messages, _, _, _):
updatedState.addReadMessagesContents((nil, messages))
case let .updateReadMessagesContents(_, messages, _, _, date):
updatedState.addReadMessagesContents((nil, messages), date: date)
case let .updateChannelReadMessagesContents(_, channelId, topMsgId, messages):
let _ = topMsgId
updatedState.addReadMessagesContents((PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), messages))
updatedState.addReadMessagesContents((PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), messages), date: nil)
case let .updateChannelMessageViews(channelId, id, views):
updatedState.addUpdateMessageImpressionCount(id: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: id), count: views)
/*case let .updateChannelMessageForwards(channelId, id, forwards):
@ -2929,7 +2929,7 @@ private func pollChannel(accountPeerId: PeerId, postbox: Postbox, network: Netwo
}, pinned: (flags & (1 << 0)) != 0)
case let .updateChannelReadMessagesContents(_, _, topMsgId, messages):
let _ = topMsgId
updatedState.addReadMessagesContents((peer.id, messages))
updatedState.addReadMessagesContents((peer.id, messages), date: nil)
case let .updateChannelMessageViews(_, id, views):
updatedState.addUpdateMessageImpressionCount(id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: id), count: views)
case let .updateChannelWebPage(_, apiWebpage, _, _):
@ -4181,16 +4181,16 @@ func replayFinalState(
transaction.setPeerPinnedThreads(peerId: peerId, threadIds: currentThreadIds)
case let .UpdatePinnedTopicOrder(peerId, threadIds):
transaction.setPeerPinnedThreads(peerId: peerId, threadIds: threadIds)
case let .ReadMessageContents(peerIdAndMessageIds):
case let .ReadMessageContents(peerIdAndMessageIds, date):
let (peerId, messageIds) = peerIdAndMessageIds
if let peerId = peerId {
for id in messageIds {
markMessageContentAsConsumedRemotely(transaction: transaction, messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: id))
markMessageContentAsConsumedRemotely(transaction: transaction, messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: id), consumeDate: date)
}
} else {
for messageId in transaction.messageIdsForGlobalIds(messageIds) {
markMessageContentAsConsumedRemotely(transaction: transaction, messageId: messageId)
markMessageContentAsConsumedRemotely(transaction: transaction, messageId: messageId, consumeDate: date)
}
}
case let .UpdateMessageImpressionCount(id, count):

View file

@ -307,7 +307,7 @@ func processSecretChatIncomingDecryptedOperations(encryptionProvider: Encryption
}
}
for messageId in messageIds {
markMessageContentAsConsumedRemotely(transaction: transaction, messageId: messageId)
markMessageContentAsConsumedRemotely(transaction: transaction, messageId: messageId, consumeDate: nil)
}
default:
break

View file

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

View file

@ -21,6 +21,7 @@ public struct UserLimitsConfiguration: Equatable {
public let maxExpiringStoriesCount: Int32
public let maxStoriesWeeklyCount: Int32
public let maxStoriesMonthlyCount: Int32
public let maxStoriesSuggestedReactions: Int32
public static var defaultValue: UserLimitsConfiguration {
return UserLimitsConfiguration(
@ -42,7 +43,8 @@ public struct UserLimitsConfiguration: Equatable {
maxStoryCaptionLength: 200,
maxExpiringStoriesCount: 3,
maxStoriesWeeklyCount: 7,
maxStoriesMonthlyCount: 30
maxStoriesMonthlyCount: 30,
maxStoriesSuggestedReactions: 1
)
}
@ -65,7 +67,8 @@ public struct UserLimitsConfiguration: Equatable {
maxStoryCaptionLength: Int32,
maxExpiringStoriesCount: Int32,
maxStoriesWeeklyCount: Int32,
maxStoriesMonthlyCount: Int32
maxStoriesMonthlyCount: Int32,
maxStoriesSuggestedReactions: Int32
) {
self.maxPinnedChatCount = maxPinnedChatCount
self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount
@ -86,6 +89,7 @@ public struct UserLimitsConfiguration: Equatable {
self.maxExpiringStoriesCount = maxExpiringStoriesCount
self.maxStoriesWeeklyCount = maxStoriesWeeklyCount
self.maxStoriesMonthlyCount = maxStoriesMonthlyCount
self.maxStoriesSuggestedReactions = maxStoriesSuggestedReactions
}
}
@ -129,5 +133,6 @@ extension UserLimitsConfiguration {
self.maxExpiringStoriesCount = getValue("story_expiring_limit", orElse: defaultValue.maxExpiringStoriesCount)
self.maxStoriesWeeklyCount = getValue("stories_sent_weekly_limit", orElse: defaultValue.maxStoriesWeeklyCount)
self.maxStoriesMonthlyCount = getValue("stories_sent_monthly_limit", orElse: defaultValue.maxStoriesMonthlyCount)
self.maxStoriesSuggestedReactions = getValue("stories_suggested_reactions_limit", orElse: defaultValue.maxStoriesMonthlyCount)
}
}

View file

@ -1,6 +1,8 @@
import Foundation
import Postbox
public let viewOnceTimeout: Int32 = 0x7fffffff
public class AutoremoveTimeoutMessageAttribute: MessageAttribute {
public let timeout: Int32
public let countdownBeginTime: Int32?
@ -54,7 +56,11 @@ public class AutoclearTimeoutMessageAttribute: MessageAttribute {
self.countdownBeginTime = countdownBeginTime
if let countdownBeginTime = countdownBeginTime {
self.automaticTimestampBasedAttribute = (1, countdownBeginTime + timeout)
if self.timeout == viewOnceTimeout {
self.automaticTimestampBasedAttribute = (1, countdownBeginTime)
} else {
self.automaticTimestampBasedAttribute = (1, countdownBeginTime + timeout)
}
} else {
self.automaticTimestampBasedAttribute = nil
}
@ -65,7 +71,11 @@ public class AutoclearTimeoutMessageAttribute: MessageAttribute {
self.countdownBeginTime = decoder.decodeOptionalInt32ForKey("c")
if let countdownBeginTime = self.countdownBeginTime {
self.automaticTimestampBasedAttribute = (1, countdownBeginTime + self.timeout)
if self.timeout == viewOnceTimeout {
self.automaticTimestampBasedAttribute = (1, countdownBeginTime)
} else {
self.automaticTimestampBasedAttribute = (1, countdownBeginTime + self.timeout)
}
} else {
self.automaticTimestampBasedAttribute = nil
}
@ -124,7 +134,7 @@ public extension Message {
guard let timeout = self.minAutoremoveOrClearTimeout else {
return false
}
if timeout > 1 * 60 {
if timeout > 1 * 60 && timeout != viewOnceTimeout {
return false
}

View file

@ -55,6 +55,7 @@ public enum EngineConfiguration {
public let maxExpiringStoriesCount: Int32
public let maxStoriesWeeklyCount: Int32
public let maxStoriesMonthlyCount: Int32
public let maxStoriesSuggestedReactions: Int32
public static var defaultValue: UserLimits {
return UserLimits(UserLimitsConfiguration.defaultValue)
@ -79,7 +80,8 @@ public enum EngineConfiguration {
maxStoryCaptionLength: Int32,
maxExpiringStoriesCount: Int32,
maxStoriesWeeklyCount: Int32,
maxStoriesMonthlyCount: Int32
maxStoriesMonthlyCount: Int32,
maxStoriesSuggestedReactions: Int32
) {
self.maxPinnedChatCount = maxPinnedChatCount
self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount
@ -100,6 +102,7 @@ public enum EngineConfiguration {
self.maxExpiringStoriesCount = maxExpiringStoriesCount
self.maxStoriesWeeklyCount = maxStoriesWeeklyCount
self.maxStoriesMonthlyCount = maxStoriesMonthlyCount
self.maxStoriesSuggestedReactions = maxStoriesSuggestedReactions
}
}
}
@ -155,7 +158,8 @@ public extension EngineConfiguration.UserLimits {
maxStoryCaptionLength: userLimitsConfiguration.maxStoryCaptionLength,
maxExpiringStoriesCount: userLimitsConfiguration.maxExpiringStoriesCount,
maxStoriesWeeklyCount: userLimitsConfiguration.maxStoriesWeeklyCount,
maxStoriesMonthlyCount: userLimitsConfiguration.maxStoriesMonthlyCount
maxStoriesMonthlyCount: userLimitsConfiguration.maxStoriesMonthlyCount,
maxStoriesSuggestedReactions: userLimitsConfiguration.maxStoriesSuggestedReactions
)
}
}

View file

@ -52,6 +52,9 @@ public final class AttachMenuBots: Equatable, Codable {
public static let hasSettings = Flags(rawValue: 1 << 0)
public static let requiresWriteAccess = Flags(rawValue: 1 << 1)
public static let showInAttachMenu = Flags(rawValue: 1 << 2)
public static let showInSettings = Flags(rawValue: 1 << 3)
public static let showInSettingsDisclaimer = Flags(rawValue: 1 << 4)
}
public struct PeerFlags: OptionSet, Codable {
@ -323,6 +326,15 @@ func managedSynchronizeAttachMenuBots(accountPeerId: PeerId, postbox: Postbox, n
if (apiFlags & (1 << 2)) != 0 {
flags.insert(.requiresWriteAccess)
}
if (apiFlags & (1 << 3)) != 0 {
flags.insert(.showInAttachMenu)
}
if (apiFlags & (1 << 4)) != 0 {
flags.insert(.showInSettings)
}
if (apiFlags & (1 << 5)) != 0 {
flags.insert(.showInSettingsDisclaimer)
}
resultBots.append(AttachMenuBots.Bot(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId)), name: name, icons: icons, peerTypes: peerTypes, flags: flags))
}
}
@ -427,13 +439,13 @@ func _internal_removeBotFromAttachMenu(accountPeerId: PeerId, postbox: Postbox,
}
public struct AttachMenuBot {
public let peer: Peer
public let peer: EnginePeer
public let shortName: String
public let icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile]
public let peerTypes: AttachMenuBots.Bot.PeerFlags
public let flags: AttachMenuBots.Bot.Flags
init(peer: Peer, shortName: String, icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile], peerTypes: AttachMenuBots.Bot.PeerFlags, flags: AttachMenuBots.Bot.Flags) {
init(peer: EnginePeer, shortName: String, icons: [AttachMenuBots.Bot.IconName: TelegramMediaFile], peerTypes: AttachMenuBots.Bot.PeerFlags, flags: AttachMenuBots.Bot.Flags) {
self.peer = peer
self.shortName = shortName
self.icons = icons
@ -450,7 +462,7 @@ func _internal_attachMenuBots(postbox: Postbox) -> Signal<[AttachMenuBot], NoErr
var resultBots: [AttachMenuBot] = []
for bot in cachedBots {
if let peer = transaction.getPeer(bot.peerId) {
resultBots.append(AttachMenuBot(peer: peer, shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, flags: bot.flags))
resultBots.append(AttachMenuBot(peer: EnginePeer(peer), shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, flags: bot.flags))
}
}
return resultBots
@ -465,7 +477,7 @@ func _internal_getAttachMenuBot(accountPeerId: PeerId, postbox: Postbox, network
return postbox.transaction { transaction -> Signal<AttachMenuBot, GetAttachMenuBotError> in
if cached, let cachedBots = cachedAttachMenuBots(transaction: transaction)?.bots {
if let bot = cachedBots.first(where: { $0.peerId == botId }), let peer = transaction.getPeer(bot.peerId) {
return .single(AttachMenuBot(peer: peer, shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, flags: bot.flags))
return .single(AttachMenuBot(peer: EnginePeer(peer), shortName: bot.name, icons: bot.icons, peerTypes: bot.peerTypes, flags: bot.flags))
}
}
@ -526,7 +538,16 @@ func _internal_getAttachMenuBot(accountPeerId: PeerId, postbox: Postbox, network
if (apiFlags & (1 << 2)) != 0 {
flags.insert(.requiresWriteAccess)
}
return .single(AttachMenuBot(peer: peer, shortName: name, icons: icons, peerTypes: peerTypes, flags: flags))
if (apiFlags & (1 << 3)) != 0 {
flags.insert(.showInAttachMenu)
}
if (apiFlags & (1 << 4)) != 0 {
flags.insert(.showInSettings)
}
if (apiFlags & (1 << 5)) != 0 {
flags.insert(.showInSettingsDisclaimer)
}
return .single(AttachMenuBot(peer: EnginePeer(peer), shortName: name, icons: icons, peerTypes: peerTypes, flags: flags))
}
}
}

View file

@ -10,11 +10,17 @@ private let botWebViewPlatform = "macos"
private let botWebViewPlatform = "ios"
#endif
public enum RequestSimpleWebViewSource {
case generic
case inline
case settings
}
public enum RequestSimpleWebViewError {
case generic
}
func _internal_requestSimpleWebView(postbox: Postbox, network: Network, botId: PeerId, url: String, inline: Bool, themeParams: [String: Any]?) -> Signal<String, RequestSimpleWebViewError> {
func _internal_requestSimpleWebView(postbox: Postbox, network: Network, botId: PeerId, url: String?, source: RequestSimpleWebViewSource, themeParams: [String: Any]?) -> Signal<String, RequestSimpleWebViewError> {
var serializedThemeParams: Api.DataJSON?
if let themeParams = themeParams, let data = try? JSONSerialization.data(withJSONObject: themeParams, options: []), let dataString = String(data: data, encoding: .utf8) {
serializedThemeParams = .dataJSON(data: dataString)
@ -28,8 +34,16 @@ func _internal_requestSimpleWebView(postbox: Postbox, network: Network, botId: P
if let _ = serializedThemeParams {
flags |= (1 << 0)
}
if inline {
switch source {
case .inline:
flags |= (1 << 1)
case .settings:
flags |= (1 << 2)
default:
break
}
if let _ = url {
flags |= (1 << 3)
}
return network.request(Api.functions.messages.requestSimpleWebView(flags: flags, bot: inputUser, url: url, startParam: nil, themeParams: serializedThemeParams, platform: botWebViewPlatform))
|> mapError { _ -> RequestSimpleWebViewError in

View file

@ -83,7 +83,7 @@ func _internal_markMessageContentAsConsumedInteractively(postbox: Postbox, messa
} else if let attribute = updatedAttributes[i] as? AutoclearTimeoutMessageAttribute {
if attribute.countdownBeginTime == nil || attribute.countdownBeginTime == 0 {
var timeout = attribute.timeout
if let duration = message.secretMediaDuration {
if let duration = message.secretMediaDuration, timeout != viewOnceTimeout {
timeout = max(timeout, Int32(duration))
}
updatedAttributes[i] = AutoclearTimeoutMessageAttribute(timeout: timeout, countdownBeginTime: timestamp)
@ -160,7 +160,7 @@ func _internal_markReactionsAsSeenInteractively(postbox: Postbox, messageId: Mes
}
}
func markMessageContentAsConsumedRemotely(transaction: Transaction, messageId: MessageId) {
func markMessageContentAsConsumedRemotely(transaction: Transaction, messageId: MessageId, consumeDate: Int32?) {
if let message = transaction.getMessage(messageId) {
var updateMessage = false
var updatedAttributes = message.attributes
@ -184,35 +184,41 @@ func markMessageContentAsConsumedRemotely(transaction: Transaction, messageId: M
}
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
let countdownBeginTime = consumeDate ?? timestamp
for i in 0 ..< updatedAttributes.count {
if let attribute = updatedAttributes[i] as? AutoremoveTimeoutMessageAttribute {
if (attribute.countdownBeginTime == nil || attribute.countdownBeginTime == 0) && message.containsSecretMedia {
updatedAttributes[i] = AutoremoveTimeoutMessageAttribute(timeout: attribute.timeout, countdownBeginTime: timestamp)
updatedAttributes[i] = AutoremoveTimeoutMessageAttribute(timeout: attribute.timeout, countdownBeginTime: countdownBeginTime)
updateMessage = true
if message.id.peerId.namespace == Namespaces.Peer.SecretChat {
} else {
for i in 0 ..< updatedMedia.count {
if let _ = updatedMedia[i] as? TelegramMediaImage {
updatedMedia[i] = TelegramMediaExpiredContent(data: .image)
} else if let _ = updatedMedia[i] as? TelegramMediaFile {
updatedMedia[i] = TelegramMediaExpiredContent(data: .file)
if attribute.timeout == viewOnceTimeout || timestamp >= countdownBeginTime + attribute.timeout {
for i in 0 ..< updatedMedia.count {
if let _ = updatedMedia[i] as? TelegramMediaImage {
updatedMedia[i] = TelegramMediaExpiredContent(data: .image)
} else if let _ = updatedMedia[i] as? TelegramMediaFile {
updatedMedia[i] = TelegramMediaExpiredContent(data: .file)
}
}
}
}
}
} else if let attribute = updatedAttributes[i] as? AutoclearTimeoutMessageAttribute {
if (attribute.countdownBeginTime == nil || attribute.countdownBeginTime == 0) && message.containsSecretMedia {
updatedAttributes[i] = AutoclearTimeoutMessageAttribute(timeout: attribute.timeout, countdownBeginTime: timestamp)
updatedAttributes[i] = AutoclearTimeoutMessageAttribute(timeout: attribute.timeout, countdownBeginTime: countdownBeginTime)
updateMessage = true
if message.id.peerId.namespace == Namespaces.Peer.SecretChat {
} else {
for i in 0 ..< updatedMedia.count {
if let _ = updatedMedia[i] as? TelegramMediaImage {
updatedMedia[i] = TelegramMediaExpiredContent(data: .image)
} else if let _ = updatedMedia[i] as? TelegramMediaFile {
updatedMedia[i] = TelegramMediaExpiredContent(data: .file)
if attribute.timeout == viewOnceTimeout || timestamp >= countdownBeginTime + attribute.timeout {
if let _ = updatedMedia[i] as? TelegramMediaImage {
updatedMedia[i] = TelegramMediaExpiredContent(data: .image)
} else if let _ = updatedMedia[i] as? TelegramMediaFile {
updatedMedia[i] = TelegramMediaExpiredContent(data: .file)
}
}
}
}

View file

@ -496,8 +496,8 @@ public extension TelegramEngine {
return _internal_requestWebView(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, peerId: peerId, botId: botId, url: url, payload: payload, themeParams: themeParams, fromMenu: fromMenu, replyToMessageId: replyToMessageId, threadId: threadId)
}
public func requestSimpleWebView(botId: PeerId, url: String, inline: Bool, themeParams: [String: Any]?) -> Signal<String, RequestSimpleWebViewError> {
return _internal_requestSimpleWebView(postbox: self.account.postbox, network: self.account.network, botId: botId, url: url, inline: inline, themeParams: themeParams)
public func requestSimpleWebView(botId: PeerId, url: String?, source: RequestSimpleWebViewSource, themeParams: [String: Any]?) -> Signal<String, RequestSimpleWebViewError> {
return _internal_requestSimpleWebView(postbox: self.account.postbox, network: self.account.network, botId: botId, url: url, source: source, themeParams: themeParams)
}
public func requestAppWebView(peerId: PeerId, appReference: BotAppReference, payload: String?, themeParams: [String: Any]?, allowWrite: Bool) -> Signal<String, RequestAppWebViewError> {

View file

@ -179,6 +179,7 @@ private enum ApplicationSpecificGlobalNotice: Int32 {
case displayChatListArchiveTooltip = 45
case displayStoryReactionTooltip = 46
case storyStealthModeReplyCount = 47
case viewOnceTooltip = 48
var key: ValueBoxKey {
let v = ValueBoxKey(length: 4)
@ -424,6 +425,10 @@ private struct ApplicationSpecificNoticeKeys {
static func storyStealthModeReplyCount() -> NoticeEntryKey {
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.storyStealthModeReplyCount.key)
}
static func viewOnceTooltip() -> NoticeEntryKey {
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.viewOnceTooltip.key)
}
}
public struct ApplicationSpecificNotice {
@ -1611,4 +1616,21 @@ public struct ApplicationSpecificNotice {
}
|> ignoreValues
}
public static func incrementViewOnceTooltip(accountManager: AccountManager<TelegramAccountManagerTypes>, count: Int = 1) -> Signal<Int, NoError> {
return accountManager.transaction { transaction -> Int in
var currentValue: Int32 = 0
if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.viewOnceTooltip())?.get(ApplicationSpecificCounterNotice.self) {
currentValue = value.value
}
let previousValue = currentValue
currentValue += Int32(count)
if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) {
transaction.setNotice(ApplicationSpecificNoticeKeys.viewOnceTooltip(), entry)
}
return Int(previousValue)
}
}
}

View file

@ -133,10 +133,7 @@ public enum PresentationResourceKey: Int32 {
case chatMediaConsumableContentIcon
case chatBubbleMediaOverlayControlSecret
case chatBubbleSecretMediaIcon
case chatBubbleSecretMediaCompactIcon
case chatInstantVideoWithWallpaperBackgroundImage
case chatInstantVideoWithoutWallpaperBackgroundImage

View file

@ -164,27 +164,7 @@ public struct PresentationResourcesChat {
return generateFilledCircleImage(diameter: 4.0, color: theme.chat.message.mediaDateAndStatusTextColor)
})
}
public static func chatBubbleSecretMediaIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatBubbleSecretMediaIcon.rawValue, { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/SecretMediaIcon"), color: theme.chat.message.mediaOverlayControlColors.foregroundColor)
})
}
public static func chatBubbleSecretMediaCompactIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatBubbleSecretMediaCompactIcon.rawValue, { theme in
if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/SecretMediaIcon"), color: theme.chat.message.mediaOverlayControlColors.foregroundColor) {
let factor: CGFloat = 0.6
return generateImage(CGSize(width: floor(image.size.width * factor), height: floor(image.size.height * factor)), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size))
})
} else {
return nil
}
})
}
public static func chatInstantVideoBackgroundImage(_ theme: PresentationTheme, wallpaper: Bool) -> UIImage? {
let key: PresentationResourceKey = !wallpaper ? PresentationResourceKey.chatInstantVideoWithoutWallpaperBackgroundImage : PresentationResourceKey.chatInstantVideoWithWallpaperBackgroundImage
return theme.image(key.rawValue, { theme in

View file

@ -379,6 +379,7 @@ swift_library(
"//submodules/TelegramUI/Components/PeerReportScreen",
"//submodules/Utils/VolumeButtons",
"//submodules/ChatContextQuery",
"//submodules/TelegramUI/Components/LegacyMessageInputPanel",
] + select({
"@build_bazel_rules_apple//apple:ios_armv7": [],
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,

View file

@ -7,7 +7,7 @@ public final class AnimatedTextComponent: Component {
public struct Item: Equatable {
public enum Content: Equatable {
case text(String)
case number(Int)
case number(Int, minDigits: Int)
}
public var id: AnyHashable
@ -86,11 +86,16 @@ public final class AnimatedTextComponent: Component {
} else {
itemText = text.map(String.init)
}
case let .number(value):
case let .number(value, minDigits):
var valueText: String = "\(value)"
while valueText.count < minDigits {
valueText.insert("0", at: valueText.startIndex)
}
if item.isUnbreakable {
itemText = ["\(value)"]
itemText = [valueText]
} else {
itemText = "\(value)".map(String.init)
itemText = valueText.map(String.init)
}
}
var index = 0

View file

@ -184,7 +184,7 @@ public final class ButtonTextContentComponent: Component {
font: Font.semibold(15.0),
color: component.badgeForeground,
items: [
AnimatedTextComponent.Item(id: AnyHashable(0), content: .number(component.badge))
AnimatedTextComponent.Item(id: AnyHashable(0), content: .number(component.badge, minDigits: 0))
]
))
)),

View file

@ -1422,6 +1422,7 @@ public class CameraScreen: ViewController {
self.authorizationStatusDisposables.dispose()
}
private var panGestureRecognizer: UIPanGestureRecognizer?
private var pipPanGestureRecognizer: UIPanGestureRecognizer?
override func didLoad() {
super.didLoad()
@ -1435,6 +1436,7 @@ public class CameraScreen: ViewController {
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
panGestureRecognizer.delegate = self
panGestureRecognizer.maximumNumberOfTouches = 1
self.panGestureRecognizer = panGestureRecognizer
self.previewContainerView.addGestureRecognizer(panGestureRecognizer)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
@ -1464,6 +1466,7 @@ public class CameraScreen: ViewController {
})
}
fileprivate var captureStartTimestamp: Double?
private func setupCamera() {
guard self.camera == nil else {
return
@ -1573,6 +1576,7 @@ public class CameraScreen: ViewController {
camera.focus(at: CGPoint(x: 0.5, y: 0.5), autoFocus: true)
camera.startCapture()
self.captureStartTimestamp = CACurrentMediaTime()
self.camera = camera
@ -1598,6 +1602,8 @@ public class CameraScreen: ViewController {
return false
}
return self.additionalPreviewContainerView.frame.contains(location)
} else if gestureRecognizer === self.panGestureRecognizer {
return true
}
return self.hasAppeared
}
@ -1628,7 +1634,7 @@ public class CameraScreen: ViewController {
case .changed:
if case .none = self.cameraState.recording {
if case .compact = layout.metrics.widthClass {
if translation.x < -10.0 || self.isDismissing {
if (translation.x < -10.0 || self.isDismissing) && self.hasAppeared {
self.isDismissing = true
let transitionFraction = 1.0 - max(0.0, translation.x * -1.0) / self.frame.width
controller.updateTransitionProgress(transitionFraction, transition: .immediate)
@ -1640,11 +1646,13 @@ public class CameraScreen: ViewController {
}
}
case .ended, .cancelled:
let velocity = gestureRecognizer.velocity(in: self.view)
let transitionFraction = 1.0 - max(0.0, translation.x * -1.0) / self.frame.width
controller.completeWithTransitionProgress(transitionFraction, velocity: abs(velocity.x), dismissing: true)
self.isDismissing = false
if self.isDismissing {
let velocity = gestureRecognizer.velocity(in: self.view)
let transitionFraction = 1.0 - max(0.0, translation.x * -1.0) / self.frame.width
controller.completeWithTransitionProgress(transitionFraction, velocity: abs(velocity.x), dismissing: true)
self.isDismissing = false
}
default:
break
}
@ -1757,7 +1765,7 @@ public class CameraScreen: ViewController {
self.backgroundView.alpha = 1.0
})
if let layout = self.validLayout, case .regular = layout.metrics.widthClass {
if let layout = self.validLayout, layout.metrics.isTablet {
self.controller?.statusBar.updateStatusBarStyle(.Hide, animated: true)
}
@ -2075,12 +2083,7 @@ public class CameraScreen: ViewController {
let isFirstTime = self.validLayout == nil
self.validLayout = layout
let isTablet: Bool
if case .regular = layout.metrics.widthClass {
isTablet = true
} else {
isTablet = false
}
let isTablet = layout.metrics.isTablet
var topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 5.0
let previewSize: CGSize
@ -2514,8 +2517,19 @@ public class CameraScreen: ViewController {
guard let self, !self.didStopCameraCapture else {
return
}
self.didStopCameraCapture = true
self.node.pauseCameraCapture()
let currentTimestamp = CACurrentMediaTime()
if let startTimestamp = self.node.captureStartTimestamp {
let difference = currentTimestamp - startTimestamp
if difference < 2.0 {
Queue.mainQueue().after(2.0 - difference) {
self.didStopCameraCapture = true
self.node.pauseCameraCapture()
}
} else {
self.didStopCameraCapture = true
self.node.pauseCameraCapture()
}
}
}
let resumeCameraCapture = { [weak self] in
@ -2615,7 +2629,7 @@ public class CameraScreen: ViewController {
self.node.camera?.stopCapture(invalidate: true)
self.isDismissed = true
if animated {
if let layout = self.validLayout, case .regular = layout.metrics.widthClass {
if let layout = self.validLayout, layout.metrics.isTablet {
self.statusBar.updateStatusBarStyle(.Ignore, animated: true)
self.node.animateOut(completion: {
self.dismiss(animated: false)
@ -2637,7 +2651,7 @@ public class CameraScreen: ViewController {
}
public func updateTransitionProgress(_ transitionFraction: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void = {}) {
if let layout = self.validLayout, case .regular = layout.metrics.widthClass {
if let layout = self.validLayout, layout.metrics.isTablet {
return
}
@ -2683,7 +2697,7 @@ public class CameraScreen: ViewController {
}
public func completeWithTransitionProgress(_ transitionFraction: CGFloat, velocity: CGFloat, dismissing: Bool) {
if let layout = self.validLayout, case .regular = layout.metrics.widthClass {
if let layout = self.validLayout, layout.metrics.isTablet {
return
}
if dismissing {

View file

@ -0,0 +1,20 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ContextReferenceButtonComponent",
module_name = "ContextReferenceButtonComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/ComponentFlow",
"//submodules/ContextUI",
],
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,124 @@
import Foundation
import UIKit
import Display
import ComponentFlow
public final class ContextReferenceButtonComponent: Component {
let content: AnyComponent<Empty>
let tag: AnyObject?
let minSize: CGSize?
let action: (UIView, ContextGesture?) -> Void
public init(
content: AnyComponent<Empty>,
tag: AnyObject? = nil,
minSize: CGSize?,
action: @escaping (UIView, ContextGesture?) -> Void
) {
self.content = content
self.tag = tag
self.minSize = minSize
self.action = action
}
public static func ==(lhs: ContextReferenceButtonComponent, rhs: ContextReferenceButtonComponent) -> Bool {
if lhs.content != rhs.content {
return false
}
if lhs.tag !== rhs.tag {
return false
}
if lhs.minSize != rhs.minSize {
return false
}
return true
}
public final class View: UIView, ComponentTaggedView {
let buttonView: HighlightableButtonNode
let sourceView: ContextControllerSourceNode
let contextContentView: ContextReferenceContentNode
private let componentView: ComponentView<Empty>
private var component: ContextReferenceButtonComponent?
public func matches(tag: Any) -> Bool {
if let component = self.component, let componentTag = component.tag {
let tag = tag as AnyObject
if componentTag === tag {
return true
}
}
return false
}
public init() {
self.componentView = ComponentView()
self.buttonView = HighlightableButtonNode()
self.sourceView = ContextControllerSourceNode()
self.contextContentView = ContextReferenceContentNode()
super.init(frame: CGRect())
self.buttonView.allowsGroupOpacity = true
self.addSubview(self.buttonView.view)
self.buttonView.addSubnode(self.sourceView)
self.sourceView.addSubnode(self.contextContentView)
self.sourceView.activated = { [weak self] gesture, _ in
if let self, let component = self.component {
component.action(self, gesture)
}
}
self.buttonView.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
@objc private func pressed() {
self.component?.action(self, nil)
}
public func update(component: ContextReferenceButtonComponent, availableSize: CGSize, transition: Transition) -> CGSize {
self.component = component
let componentSize = self.componentView.update(
transition: transition,
component: component.content,
environment: {},
containerSize: availableSize
)
var size = componentSize
if let minSize = component.minSize {
size.width = max(size.width, minSize.width)
size.height = max(size.height, minSize.height)
}
if let componentView = self.componentView.view {
componentView.isUserInteractionEnabled = false
if componentView.superview == nil {
self.contextContentView.view.addSubview(componentView)
}
transition.setFrame(view: componentView, frame: CGRect(origin: CGPoint(x: floor((size.width - componentSize.width) / 2.0), y: floor((size.height - componentSize.height) / 2.0)), size: componentSize))
}
transition.setFrame(view: self.buttonView.view, frame: CGRect(origin: .zero, size: size))
transition.setFrame(view: self.sourceView.view, frame: CGRect(origin: .zero, size: size))
transition.setFrame(view: self.contextContentView.view, frame: CGRect(origin: .zero, size: size))
return size
}
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}

View file

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

View file

@ -0,0 +1,376 @@
import Foundation
import UIKit
import AsyncDisplayKit
import LegacyComponents
import Display
import TelegramCore
import Postbox
import SwiftSignalKit
import AccountContext
import LegacyComponents
import ComponentFlow
import MessageInputPanelComponent
import TelegramPresentationData
import ContextUI
import TooltipUI
public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
private let context: AccountContext
private let chatLocation: ChatLocation
private let present: (ViewController) -> Void
private let presentInGlobalOverlay: (ViewController) -> Void
private let state = ComponentState()
private let inputPanelExternalState = MessageInputPanelComponent.ExternalState()
private let inputPanel = ComponentView<Empty>()
private var currentTimeout: Int32?
private var currentIsEditing = false
private var currentHeight: CGFloat?
private let hapticFeedback = HapticFeedback()
private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, keyboardHeight: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, metrics: LayoutMetrics)?
public init(
context: AccountContext,
chatLocation: ChatLocation,
present: @escaping (ViewController) -> Void,
presentInGlobalOverlay: @escaping (ViewController) -> Void
) {
self.context = context
self.chatLocation = chatLocation
self.present = present
self.presentInGlobalOverlay = presentInGlobalOverlay
super.init()
self.state._updated = { [weak self] transition in
if let self {
self.update(transition: transition.containedViewLayoutTransition)
}
}
}
public var sendPressed: ((NSAttributedString?) -> Void)?
public var focusUpdated: ((Bool) -> Void)?
public var heightUpdated: ((Bool) -> Void)?
public var timerUpdated: ((NSNumber?) -> Void)?
public func updateLayoutSize(_ size: CGSize, keyboardHeight: CGFloat, sideInset: CGFloat, animated: Bool) -> CGFloat {
return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, keyboardHeight: keyboardHeight, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), isMediaInputExpanded: false)
}
public func caption() -> NSAttributedString {
if let view = self.inputPanel.view as? MessageInputPanelComponent.View, case let .text(caption) = view.getSendMessageInput() {
return caption
} else {
return NSAttributedString()
}
}
private var scheduledMessageInput: MessageInputPanelComponent.SendMessageInput?
public func setCaption(_ caption: NSAttributedString?) {
let sendMessageInput = MessageInputPanelComponent.SendMessageInput.text(caption ?? NSAttributedString())
if let view = self.inputPanel.view as? MessageInputPanelComponent.View {
view.setSendMessageInput(value: sendMessageInput, updateState: true)
} else {
self.scheduledMessageInput = sendMessageInput
}
}
public func animate(_ view: UIView, frame: CGRect) {
let transition = Transition.spring(duration: 0.4)
transition.setFrame(view: view, frame: frame)
}
public func setTimeout(_ timeout: Int32) {
self.dismissTimeoutTooltip()
var timeout: Int32? = timeout
if timeout == 0 {
timeout = nil
}
self.currentTimeout = timeout
}
public func dismissInput() {
if let view = self.inputPanel.view as? MessageInputPanelComponent.View {
view.deactivateInput()
}
}
public func onAnimateOut() {
self.tooltipController?.dismiss()
}
public func baseHeight() -> CGFloat {
return 52.0
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func update(transition: ContainedViewLayoutTransition) {
if let (width, leftInset, rightInset, bottomInset, keyboardHeight, additionalSideInsets, maxHeight, isSecondary, metrics) = self.validLayout {
let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, keyboardHeight: keyboardHeight, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: transition, metrics: metrics, isMediaInputExpanded: false)
}
}
public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, keyboardHeight: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, metrics: LayoutMetrics, isMediaInputExpanded: Bool) -> CGFloat {
let previousLayout = self.validLayout
self.validLayout = (width, leftInset, rightInset, bottomInset, keyboardHeight, additionalSideInsets, maxHeight, isSecondary, metrics)
var transition = transition
if keyboardHeight.isZero, let previousKeyboardHeight = previousLayout?.keyboardHeight, previousKeyboardHeight > 0.0, !transition.isAnimated {
transition = .animated(duration: 0.4, curve: .spring)
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let theme = defaultDarkColorPresentationTheme
var timeoutValue: String
var timeoutSelected = false
if let timeout = self.currentTimeout {
if timeout == viewOnceTimeout {
timeoutValue = "1"
} else {
timeoutValue = "\(timeout)"
}
timeoutSelected = true
} else {
timeoutValue = "1"
}
var maxInputPanelHeight = maxHeight
if keyboardHeight.isZero {
maxInputPanelHeight = 60.0
}
var resetInputContents: MessageInputPanelComponent.SendMessageInput?
if let scheduledMessageInput = self.scheduledMessageInput {
resetInputContents = scheduledMessageInput
self.scheduledMessageInput = nil
}
self.inputPanel.parentState = self.state
let inputPanelSize = self.inputPanel.update(
transition: Transition(transition),
component: AnyComponent(
MessageInputPanelComponent(
externalState: self.inputPanelExternalState,
context: self.context,
theme: theme,
strings: presentationData.strings,
style: .media,
placeholder: .plain(presentationData.strings.MediaPicker_AddCaption),
maxLength: 1024,
queryTypes: [.mention],
alwaysDarkWhenHasText: false,
resetInputContents: resetInputContents,
nextInputMode: { _ in
return .emoji
},
areVoiceMessagesAvailable: false,
presentController: self.present,
presentInGlobalOverlay: self.presentInGlobalOverlay,
sendMessageAction: { [weak self] in
if let self {
self.sendPressed?(self.caption())
self.dismissInput()
}
},
sendMessageOptionsAction: nil,
sendStickerAction: { _ in },
setMediaRecordingActive: nil,
lockMediaRecording: nil,
stopAndPreviewMediaRecording: nil,
discardMediaRecordingPreview: nil,
attachmentAction: nil,
myReaction: nil,
likeAction: nil,
likeOptionsAction: nil,
inputModeAction: nil,
timeoutAction: self.chatLocation.peerId?.namespace == Namespaces.Peer.CloudUser ? { [weak self] sourceView, gesture in
if let self {
self.presentTimeoutSetup(sourceView: sourceView, gesture: gesture)
}
} : nil,
forwardAction: nil,
moreAction: nil,
presentVoiceMessagesUnavailableTooltip: nil,
presentTextLengthLimitTooltip: nil,
presentTextFormattingTooltip: nil,
paste: { _ in },
audioRecorder: nil,
videoRecordingStatus: nil,
isRecordingLocked: false,
recordedAudioPreview: nil,
hasRecordedVideoPreview: false,
wasRecordingDismissed: false,
timeoutValue: timeoutValue,
timeoutSelected: timeoutSelected,
displayGradient: false,
bottomInset: 0.0,
isFormattingLocked: false,
hideKeyboard: false,
forceIsEditing: false,
disabledPlaceholder: nil,
isChannel: false,
storyItem: nil,
chatLocation: self.chatLocation
)
),
environment: {},
containerSize: CGSize(width: width, height: maxInputPanelHeight)
)
if let view = self.inputPanel.view {
if view.superview == nil {
self.view.addSubview(view)
}
let inputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: -8.0), size: inputPanelSize)
transition.updateFrame(view: view, frame: inputPanelFrame)
}
if self.currentIsEditing != self.inputPanelExternalState.isEditing {
self.currentIsEditing = self.inputPanelExternalState.isEditing
self.focusUpdated?(self.currentIsEditing)
}
if self.currentHeight != inputPanelSize.height {
self.currentHeight = inputPanelSize.height
self.heightUpdated?(transition.isAnimated)
}
return inputPanelSize.height - 8.0
}
private func presentTimeoutSetup(sourceView: UIView, gesture: ContextGesture?) {
self.hapticFeedback.impact(.light)
var items: [ContextMenuItem] = []
let updateTimeout: (Int32?) -> Void = { [weak self] timeout in
if let self {
self.currentTimeout = timeout
self.timerUpdated?(timeout as? NSNumber)
self.update(transition: .immediate)
self.presentTimeoutTooltip(sourceView: sourceView, timeout: timeout)
}
}
let currentValue = self.currentTimeout
let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme)
let title = presentationData.strings.MediaPicker_Timer_Description
let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil
items.append(.action(ContextMenuActionItem(text: title, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction)))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPicker_Timer_ViewOnce, icon: { theme in
return currentValue == viewOnceTimeout ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, action: { _, a in
a(.default)
updateTimeout(viewOnceTimeout)
})))
let values: [Int32] = [3, 10, 30]
for value in values {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPicker_Timer_Seconds(value), icon: { theme in
return currentValue == value ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, action: { _, a in
a(.default)
updateTimeout(value)
})))
}
items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPicker_Timer_DoNotDelete, icon: { theme in
return currentValue == nil ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, action: { _, a in
a(.default)
updateTimeout(nil)
})))
let contextController = ContextController(account: self.context.account, presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
self.present(contextController)
}
private weak var tooltipController: TooltipScreen?
private func dismissTimeoutTooltip() {
if let tooltipController = self.tooltipController {
self.tooltipController = nil
tooltipController.dismiss()
}
}
private func presentTimeoutTooltip(sourceView: UIView, timeout: Int32?) {
guard let superview = self.view.superview?.superview else {
return
}
self.dismissTimeoutTooltip()
let parentFrame = superview.convert(superview.bounds, to: nil)
let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 2.0), size: CGSize())
let isVideo = !"".isEmpty
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let text: String
let iconName: String
if timeout == viewOnceTimeout {
text = isVideo ? presentationData.strings.MediaPicker_Timer_Video_ViewOnceTooltip : presentationData.strings.MediaPicker_Timer_Photo_ViewOnceTooltip
iconName = "anim_autoremove_on"
} else if let timeout {
text = isVideo ? presentationData.strings.MediaPicker_Timer_Video_TimerTooltip("\(timeout)").string : presentationData.strings.MediaPicker_Timer_Photo_TimerTooltip("\(timeout)").string
iconName = "anim_autoremove_on"
} else {
text = isVideo ? presentationData.strings.MediaPicker_Timer_Video_KeepTooltip : presentationData.strings.MediaPicker_Timer_Photo_KeepTooltip
iconName = "anim_autoremove_off"
}
let tooltipController = TooltipScreen(
account: self.context.account,
sharedContext: self.context.sharedContext,
text: .plain(text: text),
balancedTextLayout: false,
style: .customBlur(UIColor(rgb: 0x18181a), 0.0),
arrowStyle: .small,
icon: .animation(name: iconName, delay: 0.1, tintColor: nil),
location: .point(location, .bottom),
displayDuration: .default,
inset: 8.0,
shouldDismissOnTouch: { _, _ in
return .ignore
}
)
self.tooltipController = tooltipController
self.present(tooltipController)
}
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if let view = self.inputPanel.view, let panelResult = view.hitTest(self.view.convert(point, to: view), with: event) {
return panelResult
}
return result
}
}
private final class HeaderContextReferenceContentSource: ContextReferenceContentSource {
private let sourceView: UIView
var keepInPlace: Bool {
return true
}
init(sourceView: UIView) {
self.sourceView = sourceView
}
func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: .top)
}
}

View file

@ -407,7 +407,6 @@ final class MediaEditorScreenComponent: Component {
guard let _ = self.inputPanel.view as? MessageInputPanelComponent.View else {
return
}
// if view.canDeactivateInput() {
self.currentInputMode = .text
if hasFirstResponder(self) {
if let view = self.inputPanel.view as? MessageInputPanelComponent.View {
@ -421,12 +420,6 @@ final class MediaEditorScreenComponent: Component {
} else {
self.state?.updated(transition: .spring(duration: 0.4).withUserData(TextFieldComponent.AnimationHint(kind: .textFocusChanged)))
}
// } else {
// if let controller = self.environment?.controller() as? MediaEditorScreen {
// controller.presentCaptionLimitPremiumSuggestion(isPremium: self.sta)
// }
// view.animateError()
// }
}
private var animatingButtons = false
@ -498,7 +491,7 @@ final class MediaEditorScreenComponent: Component {
func animateOut(to source: TransitionAnimationSource) {
self.isDismissed = true
let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
if let view = self.cancelButton.view {
transition.setAlpha(view: view, alpha: 0.0)
@ -965,7 +958,6 @@ final class MediaEditorScreenComponent: Component {
}
var timeoutValue: String
let timeoutSelected: Bool
switch component.privacy.timeout {
case 21600:
timeoutValue = "6"
@ -978,7 +970,6 @@ final class MediaEditorScreenComponent: Component {
default:
timeoutValue = "24"
}
timeoutSelected = false
var inputPanelAvailableWidth = previewSize.width
var inputPanelAvailableHeight = 103.0
@ -1100,10 +1091,11 @@ final class MediaEditorScreenComponent: Component {
theme: environment.theme,
strings: environment.strings,
style: .editor,
placeholder: environment.strings.Story_Editor_InputPlaceholderAddCaption,
placeholder: .plain(environment.strings.Story_Editor_InputPlaceholderAddCaption),
maxLength: Int(component.context.userLimits.maxStoryCaptionLength),
queryTypes: [.mention],
alwaysDarkWhenHasText: false,
resetInputContents: nil,
nextInputMode: { _ in return nextInputMode },
areVoiceMessagesAvailable: false,
presentController: { [weak self] c in
@ -1151,7 +1143,7 @@ final class MediaEditorScreenComponent: Component {
}
}
},
timeoutAction: isEditingStory ? nil : { [weak self] view in
timeoutAction: isEditingStory ? nil : { [weak self] view, gesture in
guard let self, let controller = self.environment?.controller() as? MediaEditorScreen else {
return
}
@ -1164,7 +1156,7 @@ final class MediaEditorScreenComponent: Component {
} else {
hasPremium = false
}
controller?.presentTimeoutSetup(sourceView: view, hasPremium: hasPremium)
controller?.presentTimeoutSetup(sourceView: view, gesture: gesture, hasPremium: hasPremium)
})
},
forwardAction: nil,
@ -1217,19 +1209,31 @@ final class MediaEditorScreenComponent: Component {
hasRecordedVideoPreview: false,
wasRecordingDismissed: false,
timeoutValue: timeoutValue,
timeoutSelected: timeoutSelected,
timeoutSelected: false,
displayGradient: false,
bottomInset: 0.0,
isFormattingLocked: !state.isPremium,
hideKeyboard: self.currentInputMode == .emoji,
forceIsEditing: self.currentInputMode == .emoji,
disabledPlaceholder: nil,
storyId: nil
isChannel: false,
storyItem: nil,
chatLocation: nil
)),
environment: {},
containerSize: CGSize(width: inputPanelAvailableWidth, height: inputPanelAvailableHeight)
)
if self.inputPanelExternalState.isEditing {
if let controller = self.environment?.controller() as? MediaEditorScreen {
if controller.node.entitiesView.hasSelection {
Queue.mainQueue().justDispatch {
controller.node.entitiesView.selectEntity(nil)
}
}
}
}
if self.inputPanelExternalState.isEditing {
if self.currentInputMode == .emoji || (inputHeight.isZero && keyboardWasHidden) {
inputHeight = max(inputHeight, environment.deviceMetrics.standardInputHeight(inLandscape: false))
@ -1298,7 +1302,6 @@ final class MediaEditorScreenComponent: Component {
transition.setAlpha(view: inputPanelView, alpha: isEditingTextEntity || component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0)
}
let displayTopButtons = !(self.inputPanelExternalState.isEditing || isEditingTextEntity || component.isDisplayingTool)
let saveContentComponent: AnyComponentWithIdentity<Empty>
@ -2423,6 +2426,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
controller.statusBar.statusBarStyle = .Ignore
self.isUserInteractionEnabled = false
if self.entitiesView.hasSelection {
self.entitiesView.selectEntity(nil)
}
let previousDimAlpha = self.backgroundDimView.alpha
self.backgroundDimView.alpha = 0.0
self.backgroundDimView.layer.animateAlpha(from: previousDimAlpha, to: 0.0, duration: 0.15)
@ -2609,7 +2616,14 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0)
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.maxY + 3.0), size: CGSize())
let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: isMuted ? self.presentationData.strings.Story_Editor_TooltipMuted : self.presentationData.strings.Story_Editor_TooltipUnmuted), location: .point(location, .top), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in
let text: String
if isMuted {
text = self.presentationData.strings.Story_Editor_TooltipMuted
} else {
text = self.presentationData.strings.Story_Editor_TooltipUnmuted
}
let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: text), location: .point(location, .top), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in
return .ignore
})
self.muteTooltip = tooltipController
@ -2788,7 +2802,15 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
location = draft.location
}
}
let locationController = storyLocationPickerController(context: self.context, location: location, completion: { [weak self] location, queryId, resultId, address, countryCode in
let locationController = storyLocationPickerController(
context: self.context,
location: location,
dismissed: { [weak self] in
if let self {
self.mediaEditor?.play()
}
},
completion: { [weak self] location, queryId, resultId, address, countryCode in
if let self {
let emojiFile: Signal<TelegramMediaFile?, NoError>
if let countryCode {
@ -2897,8 +2919,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
}
}
private var drawingScreen: DrawingScreen?
private var stickerScreen: StickerPickerScreen?
fileprivate var drawingScreen: DrawingScreen?
fileprivate var stickerScreen: StickerPickerScreen?
private var defaultToEmoji = false
private var previousDrawingData: Data?
@ -3000,36 +3022,36 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
case .sticker:
self.mediaEditor?.stop()
let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get(), defaultToEmoji: self.defaultToEmoji, hasGifs: true)
controller.completion = { [weak self] content in
if let self {
if let content {
let stickerEntity = DrawingStickerEntity(content: content)
let scale: CGFloat
if case .image = content {
scale = 2.5
} else if case .video = content {
scale = 2.5
} else {
scale = 1.33
}
self.interaction?.insertEntity(stickerEntity, scale: scale)
self.hasAnyChanges = true
self.controller?.isSavingAvailable = true
self.controller?.requestLayout(transition: .immediate)
if case let .file(file) = content {
if file.isCustomEmoji {
self.defaultToEmoji = true
} else {
self.defaultToEmoji = false
}
}
}
self.stickerScreen = nil
self.mediaEditor?.play()
}
}
// controller.completion = { [weak self] content in
// if let self {
// if let content {
// if case let .file(file, _) = content {
// if file.isCustomEmoji {
// self.defaultToEmoji = true
// } else {
// self.defaultToEmoji = false
// }
// }
//
// let stickerEntity = DrawingStickerEntity(content: content)
// let scale: CGFloat
// if case .image = content {
// scale = 2.5
// } else if case .video = content {
// scale = 2.5
// } else {
// scale = 1.33
// }
// self.interaction?.insertEntity(stickerEntity, scale: scale)
//
// self.hasAnyChanges = true
// self.controller?.isSavingAvailable = true
// self.controller?.requestLayout(transition: .immediate)
// }
// self.stickerScreen = nil
// self.mediaEditor?.play()
// }
// }
controller.customModalStyleOverlayTransitionFactorUpdated = { [weak self, weak controller] transition in
if let self, let controller {
let transitionFactor = controller.modalStyleOverlayTransitionFactor
@ -3053,6 +3075,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
self.controller?.present(controller, in: .window(.root))
return
case .text:
self.mediaEditor?.stop()
self.insertTextEntity()
self.hasAnyChanges = true
@ -3542,6 +3565,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
})
}
)
controller.customModalStyleOverlayTransitionFactorUpdated = { [weak self, weak controller] transition in
if let self, let controller {
let transitionFactor = controller.modalStyleOverlayTransitionFactor
self.node.updateModalTransitionFactor(transitionFactor, transition: transition)
}
}
controller.dismissed = {
self.node.mediaEditor?.play()
}
@ -3593,6 +3622,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
editCategory: { _, _, _ in },
editBlockedPeers: { _, _, _ in }
)
controller.customModalStyleOverlayTransitionFactorUpdated = { [weak self, weak controller] transition in
if let self, let controller {
let transitionFactor = controller.modalStyleOverlayTransitionFactor
self.node.updateModalTransitionFactor(transitionFactor, transition: transition)
}
}
controller.dismissed = {
self.node.mediaEditor?.play()
}
@ -3600,7 +3635,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
})
}
func presentTimeoutSetup(sourceView: UIView, hasPremium: Bool) {
func presentTimeoutSetup(sourceView: UIView, gesture: ContextGesture?, hasPremium: Bool) {
self.hapticFeedback.impact(.light)
var items: [ContextMenuItem] = []
@ -3615,7 +3650,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme)
let title = presentationData.strings.Story_Editor_ExpirationText
let currentValue = self.state.privacy.timeout
let currentArchived = self.state.privacy.pin
let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil
items.append(.action(ContextMenuActionItem(text: title, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction)))
@ -3651,7 +3685,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
}
})))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Story_Editor_ExpirationValue(24), icon: { theme in
return currentValue == 86400 && !currentArchived ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
return currentValue == 86400 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, action: { _, a in
a(.default)
@ -3673,7 +3707,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
}
})))
let contextController = ContextController(account: self.context.account, presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil)
let contextController = ContextController(account: self.context.account, presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
self.present(contextController, in: .window(.root))
}
@ -3694,7 +3728,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
)
self.present(controller, in: .current)
}
fileprivate func presentCaptionLimitPremiumSuggestion(isPremium: Bool) {
self.dismissAllTooltips()

View file

@ -1005,12 +1005,7 @@ public final class MediaToolsScreen: ViewController {
let isFirstTime = self.validLayout == nil
self.validLayout = layout
let isTablet: Bool
if case .regular = layout.metrics.widthClass {
isTablet = true
} else {
isTablet = false
}
let isTablet = layout.metrics.isTablet
let previewSize: CGSize
let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 5.0

View file

@ -250,10 +250,11 @@ final class StoryPreviewComponent: Component {
theme: presentationData.theme,
strings: presentationData.strings,
style: .story,
placeholder: presentationData.strings.Story_InputPlaceholderReplyPrivately,
placeholder: .plain(presentationData.strings.Story_InputPlaceholderReplyPrivately),
maxLength: nil,
queryTypes: [],
alwaysDarkWhenHasText: false,
resetInputContents: nil,
nextInputMode: { _ in return .stickers },
areVoiceMessagesAvailable: false,
presentController: { _ in },
@ -291,7 +292,9 @@ final class StoryPreviewComponent: Component {
hideKeyboard: false,
forceIsEditing: false,
disabledPlaceholder: nil,
storyId: nil
isChannel: false,
storyItem: nil,
chatLocation: nil
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 200.0)

View file

@ -33,6 +33,11 @@ swift_library(
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
"//submodules/StickerPeekUI",
"//submodules/Components/ReactionButtonListComponent",
"//submodules/TelegramUI/Components/AnimatedTextComponent",
"//submodules/AnimatedCountLabelNode",
"//submodules/SearchPeerMembers",
"//submodules/ContextUI",
"//submodules/TelegramUI/Components/ContextReferenceButtonComponent",
],
visibility = [
"//visibility:public",

View file

@ -5,6 +5,7 @@ import TextFieldComponent
import ChatContextQuery
import AccountContext
import TelegramUIPreferences
import SearchPeerMembers
func textInputStateContextQueryRangeAndType(inputState: TextFieldComponent.InputState) -> [(NSRange, PossibleContextQueryTypes, NSRange?)] {
return textInputStateContextQueryRangeAndType(inputText: inputState.inputText, selectionRange: inputState.selectionRange)
@ -38,7 +39,7 @@ func inputContextQueries(_ inputState: TextFieldComponent.InputState) -> [ChatPr
return result
}
func contextQueryResultState(context: AccountContext, inputState: TextFieldComponent.InputState, availableTypes: [ChatPresentationInputQueryKind], currentQueryStates: inout [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)]) -> [ChatPresentationInputQueryKind: ChatContextQueryUpdate] {
func contextQueryResultState(context: AccountContext, inputState: TextFieldComponent.InputState, availableTypes: [ChatPresentationInputQueryKind], chatLocation: ChatLocation?, currentQueryStates: inout [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)]) -> [ChatPresentationInputQueryKind: ChatContextQueryUpdate] {
let inputQueries = inputContextQueries(inputState).filter({ query in
return availableTypes.contains(query.kind)
})
@ -48,7 +49,7 @@ func contextQueryResultState(context: AccountContext, inputState: TextFieldCompo
for query in inputQueries {
let previousQuery = currentQueryStates[query.kind]?.0
if previousQuery != query {
let signal = updatedContextQueryResultStateForQuery(context: context, inputQuery: query, previousQuery: previousQuery)
let signal = updatedContextQueryResultStateForQuery(context: context, chatLocation: chatLocation, inputQuery: query, previousQuery: previousQuery)
updates[query.kind] = .update(query, signal)
}
}
@ -69,7 +70,7 @@ func contextQueryResultState(context: AccountContext, inputState: TextFieldCompo
return updates
}
private func updatedContextQueryResultStateForQuery(context: AccountContext, inputQuery: ChatPresentationInputQuery, previousQuery: ChatPresentationInputQuery?) -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> {
private func updatedContextQueryResultStateForQuery(context: AccountContext, chatLocation: ChatLocation?, inputQuery: ChatPresentationInputQuery, previousQuery: ChatPresentationInputQuery?) -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> {
switch inputQuery {
case let .emoji(query):
var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = .complete()
@ -149,7 +150,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, inp
|> castError(ChatContextQueryError.self)
return signal |> then(hashtags)
case let .mention(query, _):
case let .mention(query, types):
var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = .complete()
if let previousQuery = previousQuery {
switch previousQuery {
@ -163,34 +164,79 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, inp
}
let normalizedQuery = query.lowercased()
if normalizedQuery.isEmpty {
let peers: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = context.engine.peers.recentPeers()
|> map { recentPeers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
if case let .peers(peers) = recentPeers {
let peers = peers.filter { peer in
return peer.addressName != nil
}.compactMap { EnginePeer($0) }
return { _ in return .mentions(peers) }
} else {
return { _ in return .mentions([]) }
}
}
|> castError(ChatContextQueryError.self)
return signal |> then(peers)
} else {
let peers: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = context.engine.contacts.searchLocalPeers(query: normalizedQuery)
|> map { peersAndPresences -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
let peers = peersAndPresences.filter { peer in
if let peer = peer.peer, case .user = peer, peer.addressName != nil {
return true
} else {
if let chatLocation, let peerId = chatLocation.peerId {
let inlineBots: Signal<[(EnginePeer, Double)], NoError> = types.contains(.contextBots) ? context.engine.peers.recentlyUsedInlineBots() : .single([])
let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings
let participants = combineLatest(inlineBots, searchPeerMembers(context: context, peerId: peerId, chatLocation: chatLocation, query: query, scope: .mention))
|> map { inlineBots, peers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
let filteredInlineBots = inlineBots.sorted(by: { $0.1 > $1.1 }).filter { peer, rating in
if rating < 0.14 {
return false
}
}.compactMap { $0.peer }
return { _ in return .mentions(peers) }
if peer.indexName.matchesByTokens(normalizedQuery) {
return true
}
if let addressName = peer.addressName, addressName.lowercased().hasPrefix(normalizedQuery) {
return true
}
return false
}.map { $0.0 }
let inlineBotPeerIds = Set(filteredInlineBots.map { $0.id })
let filteredPeers = peers.filter { peer in
if inlineBotPeerIds.contains(peer.id) {
return false
}
if !types.contains(.accountPeer) && peer.id == context.account.peerId {
return false
}
return true
}
var sortedPeers = filteredInlineBots
sortedPeers.append(contentsOf: filteredPeers.sorted(by: { lhs, rhs in
let result = lhs.indexName.stringRepresentation(lastNameFirst: true).compare(rhs.indexName.stringRepresentation(lastNameFirst: true))
return result == .orderedAscending
}))
sortedPeers = sortedPeers.filter { peer in
return !peer.displayTitle(strings: strings, displayOrder: .firstLast).isEmpty
}
return { _ in return .mentions(sortedPeers) }
}
|> castError(ChatContextQueryError.self)
return signal |> then(peers)
return signal |> then(participants)
} else {
if normalizedQuery.isEmpty {
let peers: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = context.engine.peers.recentPeers()
|> map { recentPeers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
if case let .peers(peers) = recentPeers {
let peers = peers.filter { peer in
return peer.addressName != nil
}.compactMap { EnginePeer($0) }
return { _ in return .mentions(peers) }
} else {
return { _ in return .mentions([]) }
}
}
|> castError(ChatContextQueryError.self)
return signal |> then(peers)
} else {
let peers: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = context.engine.contacts.searchLocalPeers(query: normalizedQuery)
|> map { peersAndPresences -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
let peers = peersAndPresences.filter { peer in
if let peer = peer.peer, case .user = peer, peer.addressName != nil {
return true
} else {
return false
}
}.compactMap { $0.peer }
return { _ in return .mentions(peers) }
}
|> castError(ChatContextQueryError.self)
return signal |> then(peers)
}
}
case let .emojiSearch(query, languageCode, range):
let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))

View file

@ -15,6 +15,11 @@ import ChatContextQuery
import TextFormat
import EmojiSuggestionsComponent
import AudioToolbox
import AnimatedTextComponent
import AnimatedCountLabelNode
import ContextReferenceButtonComponent
private let timeoutButtonTag = GenericComponentViewTag()
public final class MessageInputPanelComponent: Component {
public struct ContextQueryTypes: OptionSet {
@ -36,6 +41,7 @@ public final class MessageInputPanelComponent: Component {
public enum Style {
case story
case editor
case media
}
public enum InputMode: Hashable {
@ -56,6 +62,26 @@ public final class MessageInputPanelComponent: Component {
}
}
public enum Placeholder: Equatable {
public enum CounterItemContent: Equatable {
case text(String)
case number(Int, minDigits: Int)
}
public struct CounterItem: Equatable {
public var id: Int
public var content: CounterItemContent
public init(id: Int, content: CounterItemContent) {
self.id = id
self.content = content
}
}
case plain(String)
case counter([CounterItem])
}
public final class ExternalState {
public fileprivate(set) var isEditing: Bool = false
public fileprivate(set) var hasText: Bool = false
@ -75,10 +101,11 @@ public final class MessageInputPanelComponent: Component {
public let theme: PresentationTheme
public let strings: PresentationStrings
public let style: Style
public let placeholder: String
public let placeholder: Placeholder
public let maxLength: Int?
public let queryTypes: ContextQueryTypes
public let alwaysDarkWhenHasText: Bool
public let resetInputContents: SendMessageInput?
public let nextInputMode: (Bool) -> InputMode?
public let areVoiceMessagesAvailable: Bool
public let presentController: (ViewController) -> Void
@ -95,7 +122,7 @@ public final class MessageInputPanelComponent: Component {
public let likeAction: (() -> Void)?
public let likeOptionsAction: ((UIView, ContextGesture?) -> Void)?
public let inputModeAction: (() -> Void)?
public let timeoutAction: ((UIView) -> Void)?
public let timeoutAction: ((UIView, ContextGesture?) -> Void)?
public let forwardAction: (() -> Void)?
public let moreAction: ((UIView, ContextGesture?) -> Void)?
public let presentVoiceMessagesUnavailableTooltip: ((UIView) -> Void)?
@ -116,7 +143,9 @@ public final class MessageInputPanelComponent: Component {
public let hideKeyboard: Bool
public let forceIsEditing: Bool
public let disabledPlaceholder: String?
public let storyId: Int32?
public let isChannel: Bool
public let storyItem: EngineStoryItem?
public let chatLocation: ChatLocation?
public init(
externalState: ExternalState,
@ -124,10 +153,11 @@ public final class MessageInputPanelComponent: Component {
theme: PresentationTheme,
strings: PresentationStrings,
style: Style,
placeholder: String,
placeholder: Placeholder,
maxLength: Int?,
queryTypes: ContextQueryTypes,
alwaysDarkWhenHasText: Bool,
resetInputContents: SendMessageInput?,
nextInputMode: @escaping (Bool) -> InputMode?,
areVoiceMessagesAvailable: Bool,
presentController: @escaping (ViewController) -> Void,
@ -144,7 +174,7 @@ public final class MessageInputPanelComponent: Component {
likeAction: (() -> Void)?,
likeOptionsAction: ((UIView, ContextGesture?) -> Void)?,
inputModeAction: (() -> Void)?,
timeoutAction: ((UIView) -> Void)?,
timeoutAction: ((UIView, ContextGesture?) -> Void)?,
forwardAction: (() -> Void)?,
moreAction: ((UIView, ContextGesture?) -> Void)?,
presentVoiceMessagesUnavailableTooltip: ((UIView) -> Void)?,
@ -165,7 +195,9 @@ public final class MessageInputPanelComponent: Component {
hideKeyboard: Bool,
forceIsEditing: Bool,
disabledPlaceholder: String?,
storyId: Int32?
isChannel: Bool,
storyItem: EngineStoryItem?,
chatLocation: ChatLocation?
) {
self.externalState = externalState
self.context = context
@ -177,6 +209,7 @@ public final class MessageInputPanelComponent: Component {
self.maxLength = maxLength
self.queryTypes = queryTypes
self.alwaysDarkWhenHasText = alwaysDarkWhenHasText
self.resetInputContents = resetInputContents
self.areVoiceMessagesAvailable = areVoiceMessagesAvailable
self.presentController = presentController
self.presentInGlobalOverlay = presentInGlobalOverlay
@ -213,7 +246,9 @@ public final class MessageInputPanelComponent: Component {
self.hideKeyboard = hideKeyboard
self.forceIsEditing = forceIsEditing
self.disabledPlaceholder = disabledPlaceholder
self.storyId = storyId
self.isChannel = isChannel
self.storyItem = storyItem
self.chatLocation = chatLocation
}
public static func ==(lhs: MessageInputPanelComponent, rhs: MessageInputPanelComponent) -> Bool {
@ -244,6 +279,9 @@ public final class MessageInputPanelComponent: Component {
if lhs.alwaysDarkWhenHasText != rhs.alwaysDarkWhenHasText {
return false
}
if lhs.resetInputContents != rhs.resetInputContents {
return false
}
if lhs.areVoiceMessagesAvailable != rhs.areVoiceMessagesAvailable {
return false
}
@ -307,13 +345,19 @@ public final class MessageInputPanelComponent: Component {
if (lhs.likeOptionsAction == nil) != (rhs.likeOptionsAction == nil) {
return false
}
if lhs.storyId != rhs.storyId {
if lhs.isChannel != rhs.isChannel {
return false
}
if lhs.storyItem != rhs.storyItem {
return false
}
if lhs.chatLocation != rhs.chatLocation {
return false
}
return true
}
public enum SendMessageInput {
public enum SendMessageInput: Equatable {
case text(NSAttributedString)
}
@ -329,6 +373,7 @@ public final class MessageInputPanelComponent: Component {
private let counter = ComponentView<Empty>()
private var disabledPlaceholder: ComponentView<Empty>?
private var textClippingView = UIView()
private let textField = ComponentView<Empty>()
private let textFieldExternalState = TextFieldComponent.ExternalState()
@ -360,11 +405,17 @@ public final class MessageInputPanelComponent: Component {
private var viewForOverlayContent: ViewForOverlayContent?
private var currentEmojiSuggestionView: ComponentHostView<Empty>?
private var viewsIconView: UIImageView?
private var viewStatsCountText: AnimatedCountLabelView?
private var reactionStatsCountText: AnimatedCountLabelView?
private let hapticFeedback = HapticFeedback()
private var component: MessageInputPanelComponent?
private weak var state: EmptyComponentState?
private var pendingSetMessageInput: SendMessageInput?
public var likeButtonView: UIView? {
return self.likeButton.view
}
@ -380,6 +431,7 @@ public final class MessageInputPanelComponent: Component {
let blurEffect = UIBlurEffect(style: style)
let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect)
let vibrancyEffectView = UIVisualEffectView(effect: vibrancyEffect)
vibrancyEffectView.alpha = 0.0
self.vibrancyEffectView = vibrancyEffectView
self.mediaRecordingVibrancyContainer = UIView()
@ -388,12 +440,15 @@ public final class MessageInputPanelComponent: Component {
self.gradientView = UIImageView()
self.bottomGradientView = UIView()
self.textClippingView.clipsToBounds = true
super.init(frame: frame)
self.addSubview(self.bottomGradientView)
self.addSubview(self.gradientView)
self.fieldBackgroundView.addSubview(self.vibrancyEffectView)
self.addSubview(self.fieldBackgroundView)
self.addSubview(self.textClippingView)
self.viewForOverlayContent = ViewForOverlayContent(
ignoreHit: { [weak self] view, point in
@ -422,6 +477,14 @@ public final class MessageInputPanelComponent: Component {
fatalError("init(coder:) has not been implemented")
}
public func hasFirstResponder() -> Bool {
if let textFieldView = self.textField.view as? TextFieldComponent.View {
return textFieldView.hasFirstResponder()
} else {
return false
}
}
public func getSendMessageInput() -> SendMessageInput {
guard let textFieldView = self.textField.view as? TextFieldComponent.View else {
return .text(NSAttributedString())
@ -430,6 +493,17 @@ public final class MessageInputPanelComponent: Component {
return .text(textFieldView.getAttributedText())
}
public func setSendMessageInput(value: SendMessageInput, updateState: Bool) {
if let textFieldView = self.textField.view as? TextFieldComponent.View {
switch value {
case let .text(text):
textFieldView.setAttributedText(text, updateState: updateState)
}
} else {
self.pendingSetMessageInput = value
}
}
public func getAttachmentButtonView() -> UIView? {
guard let attachmentButtonView = self.attachmentButton.view else {
return nil
@ -437,9 +511,9 @@ public final class MessageInputPanelComponent: Component {
return attachmentButtonView
}
public func clearSendMessageInput() {
public func clearSendMessageInput(updateState: Bool) {
if let textFieldView = self.textField.view as? TextFieldComponent.View {
textFieldView.setAttributedText(NSAttributedString())
textFieldView.setAttributedText(NSAttributedString(), updateState: updateState)
}
}
@ -498,7 +572,7 @@ public final class MessageInputPanelComponent: Component {
if component.queryTypes.contains(.emoji) {
availableTypes.append(.emoji)
}
let contextQueryUpdates = contextQueryResultState(context: context, inputState: inputState, availableTypes: availableTypes, currentQueryStates: &self.contextQueryStates)
let contextQueryUpdates = contextQueryResultState(context: context, inputState: inputState, availableTypes: availableTypes, chatLocation: component.chatLocation, currentQueryStates: &self.contextQueryStates)
for (kind, update) in contextQueryUpdates {
switch update {
@ -560,6 +634,8 @@ public final class MessageInputPanelComponent: Component {
}
func update(component: MessageInputPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let previousPlaceholder = self.component?.placeholder
var insets = UIEdgeInsets(top: 14.0, left: 9.0, bottom: 6.0, right: 41.0)
if let _ = component.attachmentAction {
@ -569,9 +645,19 @@ public final class MessageInputPanelComponent: Component {
insets.right = 41.0
}
let mediaInsets = UIEdgeInsets(top: insets.top, left: 9.0, bottom: insets.bottom, right: 41.0)
var textFieldSideInset = 9.0
if case .media = component.style {
textFieldSideInset = 8.0
}
let mediaInsets = UIEdgeInsets(top: insets.top, left: textFieldSideInset, bottom: insets.bottom, right: 41.0)
let baseFieldHeight: CGFloat = 40.0
var transition = transition
if transition.animation.isImmediate, let previousComponent = self.component, previousComponent.storyItem?.id == component.storyItem?.id, component.isChannel {
transition = transition.withAnimation(.curve(duration: 0.3, curve: .spring))
}
self.component = component
self.state = state
@ -626,6 +712,13 @@ public final class MessageInputPanelComponent: Component {
textColor: UIColor(rgb: 0xffffff),
insets: UIEdgeInsets(top: 9.0, left: 8.0, bottom: 10.0, right: 48.0),
hideKeyboard: component.hideKeyboard,
resetText: component.resetInputContents.flatMap { resetInputContents in
switch resetInputContents {
case let .text(value):
return value
}
},
isOneLineWhenUnfocused: component.style == .media,
formatMenuAvailability: component.isFormattingLocked ? .locked : .available,
lockedFormatAction: {
component.presentTextFormattingTooltip?()
@ -642,23 +735,39 @@ public final class MessageInputPanelComponent: Component {
)
let isEditing = self.textFieldExternalState.isEditing || component.forceIsEditing
var placeholderItems: [AnimatedTextComponent.Item] = []
switch component.placeholder {
case let .plain(string):
placeholderItems.append(AnimatedTextComponent.Item(id: AnyHashable(0 as Int), content: .text(string)))
case let .counter(items):
for item in items {
switch item.content {
case let .text(string):
placeholderItems.append(AnimatedTextComponent.Item(id: AnyHashable(item.id), content: .text(string)))
case let .number(value, minDigits):
placeholderItems.append(AnimatedTextComponent.Item(id: AnyHashable(item.id), content: .number(value, minDigits: minDigits)))
}
}
}
let placeholderTransition: Transition = (previousPlaceholder != nil && previousPlaceholder != component.placeholder) ? Transition(animation: .curve(duration: 0.3, curve: .spring)) : .immediate
let placeholderSize = self.placeholder.update(
transition: .immediate,
component: AnyComponent(Text(
text: component.placeholder,
transition: placeholderTransition,
component: AnyComponent(AnimatedTextComponent(
font: Font.regular(17.0),
color: UIColor(rgb: 0xffffff, alpha: 0.3)
color: UIColor(rgb: 0xffffff, alpha: 0.3),
items: placeholderItems
)),
environment: {},
containerSize: availableTextFieldSize
)
let _ = self.vibrancyPlaceholder.update(
transition: .immediate,
component: AnyComponent(Text(
text: component.placeholder,
transition: placeholderTransition,
component: AnyComponent(AnimatedTextComponent(
font: Font.regular(17.0),
color: .white
color: .white,
items: placeholderItems
)),
environment: {},
containerSize: availableTextFieldSize
@ -672,20 +781,33 @@ public final class MessageInputPanelComponent: Component {
var fieldBackgroundFrame: CGRect
if hasMediaRecording {
fieldBackgroundFrame = CGRect(origin: CGPoint(x: mediaInsets.left, y: insets.top), size: CGSize(width: availableSize.width - mediaInsets.left - mediaInsets.right, height: textFieldSize.height))
} else if isEditing {
} else if isEditing || component.style == .editor || component.style == .media {
fieldBackgroundFrame = fieldFrame
} else {
fieldBackgroundFrame = CGRect(origin: CGPoint(x: mediaInsets.left, y: insets.top), size: CGSize(width: availableSize.width - mediaInsets.left - insets.right, height: textFieldSize.height))
if component.likeAction != nil && component.forwardAction != nil {
fieldBackgroundFrame.size.width -= 49.0
if component.forwardAction != nil && component.likeAction != nil {
fieldBackgroundFrame = CGRect(origin: CGPoint(x: mediaInsets.left, y: insets.top), size: CGSize(width: availableSize.width - mediaInsets.left - insets.right - 49.0, height: textFieldSize.height))
} else if component.forwardAction != nil {
fieldBackgroundFrame = CGRect(origin: CGPoint(x: mediaInsets.left, y: insets.top), size: CGSize(width: availableSize.width - mediaInsets.left - insets.right, height: textFieldSize.height))
} else {
fieldBackgroundFrame = CGRect(origin: CGPoint(x: mediaInsets.left, y: insets.top), size: CGSize(width: availableSize.width - mediaInsets.left - 50.0, height: textFieldSize.height))
}
}
transition.setFrame(view: self.vibrancyEffectView, frame: CGRect(origin: CGPoint(), size: fieldBackgroundFrame.size))
self.vibrancyEffectView.isHidden = component.style == .media
if isEditing {
self.vibrancyEffectView.alpha = 1.0
}
transition.setFrame(view: self.fieldBackgroundView, frame: fieldBackgroundFrame)
self.fieldBackgroundView.update(size: fieldBackgroundFrame.size, cornerRadius: baseFieldHeight * 0.5, transition: transition.containedViewLayoutTransition)
var textClippingFrame = fieldBackgroundFrame
if component.style == .media, !isEditing {
textClippingFrame.size.height -= 10.0
}
transition.setFrame(view: self.textClippingView, frame: textClippingFrame)
let gradientFrame = CGRect(origin: CGPoint(x: fieldBackgroundFrame.minX - fieldFrame.minX, y: -topGradientHeight), size: CGSize(width: availableSize.width - (fieldBackgroundFrame.minX - fieldFrame.minX), height: topGradientHeight + fieldBackgroundFrame.maxY + insets.bottom))
transition.setFrame(view: self.gradientView, frame: gradientFrame)
transition.setFrame(view: self.bottomGradientView, frame: CGRect(origin: CGPoint(x: 0.0, y: gradientFrame.maxY), size: CGSize(width: availableSize.width, height: component.bottomInset)))
@ -715,32 +837,120 @@ public final class MessageInputPanelComponent: Component {
transition.setPosition(view: placeholderView, position: placeholderFrame.origin)
placeholderView.bounds = CGRect(origin: CGPoint(), size: placeholderFrame.size)
transition.setAlpha(view: placeholderView, alpha: (hasMediaRecording || hasMediaEditing || component.disabledPlaceholder != nil) ? 0.0 : 1.0)
transition.setAlpha(view: vibrancyPlaceholderView, alpha: (hasMediaRecording || hasMediaEditing || component.disabledPlaceholder != nil) ? 0.0 : 1.0)
transition.setAlpha(view: placeholderView, alpha: (hasMediaRecording || hasMediaEditing || component.disabledPlaceholder != nil || component.isChannel) ? 0.0 : 1.0)
transition.setAlpha(view: vibrancyPlaceholderView, alpha: (hasMediaRecording || hasMediaEditing || component.disabledPlaceholder != nil || component.isChannel) ? 0.0 : 1.0)
}
transition.setAlpha(view: self.fieldBackgroundView, alpha: component.disabledPlaceholder != nil ? 0.0 : 1.0)
transition.setAlpha(view: self.fieldBackgroundView, alpha: (component.disabledPlaceholder != nil || component.isChannel) ? 0.0 : 1.0)
let size = CGSize(width: availableSize.width, height: textFieldSize.height + insets.top + insets.bottom)
if let textFieldView = self.textField.view {
var rightButtonsOffsetX: CGFloat = 0.0
if component.isChannel, let storyItem = component.storyItem {
var viewsTransition = transition
let viewsIconView: UIImageView
if let current = self.viewsIconView {
viewsIconView = current
} else {
viewsTransition = viewsTransition.withAnimation(.none)
viewsIconView = UIImageView(image: UIImage(bundleImageName: "Stories/EmbeddedViewIcon"))
self.viewsIconView = viewsIconView
self.addSubview(viewsIconView)
}
let viewStatsCountText: AnimatedCountLabelView
if let current = self.viewStatsCountText {
viewStatsCountText = current
} else {
viewStatsCountText = AnimatedCountLabelView(frame: CGRect())
self.viewStatsCountText = viewStatsCountText
self.addSubview(viewStatsCountText)
}
let reactionStatsCountText: AnimatedCountLabelView
if let current = self.reactionStatsCountText {
reactionStatsCountText = current
} else {
reactionStatsCountText = AnimatedCountLabelView(frame: CGRect())
self.reactionStatsCountText = reactionStatsCountText
self.addSubview(reactionStatsCountText)
}
var viewCount = storyItem.views?.seenCount ?? 0
if viewCount == 0 {
viewCount = 1
}
var reactionCount = storyItem.views?.reactedCount ?? 0
if reactionCount == 0, storyItem.myReaction != nil {
reactionCount += 1
}
var regularSegments: [AnimatedCountLabelView.Segment] = []
regularSegments.append(.number(viewCount, NSAttributedString(string: "\(viewCount)", font: Font.with(size: 15.0, traits: .monospacedNumbers), textColor: .white)))
var reactionSegments: [AnimatedCountLabelView.Segment] = []
reactionSegments.append(.number(reactionCount, NSAttributedString(string: "\(reactionCount)", font: Font.with(size: 15.0, traits: .monospacedNumbers), textColor: .white)))
let viewStatsTextLayout = viewStatsCountText.update(size: CGSize(width: availableSize.width, height: size.height), segments: regularSegments, transition: viewsTransition.containedViewLayoutTransition)
let reactionStatsTextLayout = reactionStatsCountText.update(size: CGSize(width: availableSize.width, height: size.height), segments: reactionSegments, transition: viewsTransition.containedViewLayoutTransition)
var contentX: CGFloat = 16.0
if let image = viewsIconView.image {
let viewsIconFrame = CGRect(origin: CGPoint(x: contentX, y: size.height - insets.bottom - baseFieldHeight + floor((baseFieldHeight - image.size.height) * 0.5)), size: image.size)
viewsTransition.setPosition(view: viewsIconView, position: viewsIconFrame.center)
viewsTransition.setBounds(view: viewsIconView, bounds: CGRect(origin: CGPoint(), size: viewsIconFrame.size))
contentX += image.size.width + 5.0
}
transition.setFrame(view: viewStatsCountText, frame: CGRect(origin: CGPoint(x: contentX, y: size.height - insets.bottom - baseFieldHeight + floor((baseFieldHeight - viewStatsTextLayout.size.height) * 0.5)), size: viewStatsTextLayout.size))
transition.setFrame(view: reactionStatsCountText, frame: CGRect(origin: CGPoint(x: availableSize.width - 11.0 - reactionStatsTextLayout.size.width, y: size.height - insets.bottom - baseFieldHeight + floor((baseFieldHeight - reactionStatsTextLayout.size.height) * 0.5)), size: reactionStatsTextLayout.size))
rightButtonsOffsetX -= reactionStatsTextLayout.size.width + 4.0
} else {
if let viewsIconView = self.viewsIconView {
self.viewsIconView = nil
viewsIconView.removeFromSuperview()
}
if let viewStatsCountText = self.viewStatsCountText {
self.viewStatsCountText = nil
viewStatsCountText.removeFromSuperview()
}
if let reactionStatsCountText = self.reactionStatsCountText {
self.reactionStatsCountText = nil
reactionStatsCountText.removeFromSuperview()
}
}
if let textFieldView = self.textField.view as? TextFieldComponent.View {
if textFieldView.superview == nil {
self.addSubview(textFieldView)
self.textClippingView.addSubview(textFieldView)
if let viewForOverlayContent = self.viewForOverlayContent {
self.addSubview(viewForOverlayContent)
}
if let pendingSetMessageInput = self.pendingSetMessageInput {
self.pendingSetMessageInput = nil
switch pendingSetMessageInput {
case let .text(text):
textFieldView.setAttributedText(text, updateState: false)
}
}
}
let textFieldFrame = CGRect(origin: CGPoint(x: fieldBackgroundFrame.minX, y: fieldBackgroundFrame.maxY - textFieldSize.height), size: textFieldSize)
let textFieldFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: textFieldSize)
transition.setFrame(view: textFieldView, frame: textFieldFrame)
transition.setAlpha(view: textFieldView, alpha: (hasMediaRecording || hasMediaEditing || component.disabledPlaceholder != nil) ? 0.0 : 1.0)
transition.setAlpha(view: textFieldView, alpha: (hasMediaRecording || hasMediaEditing || component.disabledPlaceholder != nil || component.isChannel) ? 0.0 : 1.0)
if let viewForOverlayContent = self.viewForOverlayContent {
transition.setFrame(view: viewForOverlayContent, frame: textFieldFrame)
}
}
if let disabledPlaceholderText = component.disabledPlaceholder {
if let disabledPlaceholderText = component.disabledPlaceholder, !component.isChannel {
let disabledPlaceholder: ComponentView<Empty>
var disabledPlaceholderTransition = transition
if let current = self.disabledPlaceholder {
@ -809,7 +1019,7 @@ public final class MessageInputPanelComponent: Component {
transition: transition,
component: AnyComponent(MessageInputActionButtonComponent(
mode: attachmentButtonMode,
storyId: component.storyId,
storyId: component.storyItem?.id,
action: { [weak self] mode, action, sendAction in
guard let self, let component = self.component, case .up = action else {
return
@ -937,6 +1147,8 @@ public final class MessageInputPanelComponent: Component {
let inputActionButtonMode: MessageInputActionButtonComponent.Mode
if case .editor = component.style {
inputActionButtonMode = isEditing ? .apply : .none
} else if case .media = component.style {
inputActionButtonMode = isEditing ? .apply : .none
} else {
if hasMediaEditing {
inputActionButtonMode = .send
@ -958,7 +1170,7 @@ public final class MessageInputPanelComponent: Component {
transition: transition,
component: AnyComponent(MessageInputActionButtonComponent(
mode: inputActionButtonMode,
storyId: component.storyId,
storyId: component.storyItem?.id,
action: { [weak self] mode, action, sendAction in
guard let self, let component = self.component else {
return
@ -1006,7 +1218,7 @@ public final class MessageInputPanelComponent: Component {
break
}
},
longPressAction: component.sendMessageOptionsAction,
longPressAction: inputActionButtonMode == .send ? component.sendMessageOptionsAction : nil,
switchMediaInputMode: { [weak self] in
guard let self else {
return
@ -1067,14 +1279,24 @@ public final class MessageInputPanelComponent: Component {
}
var inputActionButtonOriginX: CGFloat
if component.setMediaRecordingActive != nil || isEditing {
inputActionButtonOriginX = fieldBackgroundFrame.maxX + floorToScreenPixels((41.0 - inputActionButtonSize.width) * 0.5)
if rightButtonsOffsetX != 0.0 {
inputActionButtonOriginX = availableSize.width - 3.0 + rightButtonsOffsetX
if displayLikeAction {
inputActionButtonOriginX -= 39.0
}
if component.forwardAction != nil {
inputActionButtonOriginX -= 46.0
}
} else {
inputActionButtonOriginX = size.width
}
if hasLikeAction {
inputActionButtonOriginX += 3.0
if component.setMediaRecordingActive != nil || isEditing {
inputActionButtonOriginX = fieldBackgroundFrame.maxX + floorToScreenPixels((41.0 - inputActionButtonSize.width) * 0.5)
} else {
inputActionButtonOriginX = size.width
}
if hasLikeAction {
inputActionButtonOriginX += 3.0
}
}
if let inputActionButtonView = self.inputActionButton.view {
@ -1086,8 +1308,14 @@ public final class MessageInputPanelComponent: Component {
transition.setBounds(view: inputActionButtonView, bounds: CGRect(origin: CGPoint(), size: inputActionButtonFrame.size))
transition.setAlpha(view: inputActionButtonView, alpha: likeActionReplacesInputAction ? 0.0 : 1.0)
if hasLikeAction {
inputActionButtonOriginX += 41.0
if rightButtonsOffsetX != 0.0 {
if hasLikeAction {
inputActionButtonOriginX += 46.0
}
} else {
if hasLikeAction {
inputActionButtonOriginX += 41.0
}
}
}
@ -1095,7 +1323,7 @@ public final class MessageInputPanelComponent: Component {
transition: transition,
component: AnyComponent(MessageInputActionButtonComponent(
mode: .like(reaction: component.myReaction?.reaction, file: component.myReaction?.file, animationFileId: component.myReaction?.animationFileId),
storyId: component.storyId,
storyId: component.storyItem?.id,
action: { [weak self] _, action, _ in
guard let self, let component = self.component else {
return
@ -1129,7 +1357,10 @@ public final class MessageInputPanelComponent: Component {
if likeButtonView.superview == nil {
self.addSubview(likeButtonView)
}
let likeButtonFrame = CGRect(origin: CGPoint(x: inputActionButtonOriginX, y: size.height - insets.bottom - baseFieldHeight + floor((baseFieldHeight - likeButtonSize.height) * 0.5)), size: likeButtonSize)
var likeButtonFrame = CGRect(origin: CGPoint(x: inputActionButtonOriginX, y: size.height - insets.bottom - baseFieldHeight + floor((baseFieldHeight - likeButtonSize.height) * 0.5)), size: likeButtonSize)
if component.forwardAction == nil && rightButtonsOffsetX == 0.0 {
likeButtonFrame.origin.x += 3.0
}
transition.setPosition(view: likeButtonView, position: likeButtonFrame.center)
transition.setBounds(view: likeButtonView, bounds: CGRect(origin: CGPoint(), size: likeButtonFrame.size))
transition.setAlpha(view: likeButtonView, alpha: displayLikeAction ? 1.0 : 0.0)
@ -1239,45 +1470,25 @@ public final class MessageInputPanelComponent: Component {
}
}
let accentColor = component.theme.chat.inputPanel.panelControlAccentColor
if let timeoutAction = component.timeoutAction, let timeoutValue = component.timeoutValue {
func generateIcon(value: String) -> UIImage? {
let image = UIImage(bundleImageName: "Media Editor/Timeout")!
let valueString = NSAttributedString(string: value, font: Font.with(size: value.count == 1 ? 12.0 : 10.0, design: .round, weight: .semibold), textColor: .white, paragraphAlignment: .center)
return generateImage(image.size, contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
if let cgImage = image.cgImage {
context.draw(cgImage, in: CGRect(origin: .zero, size: size))
}
var offset: CGPoint = CGPoint(x: 0.0, y: -3.0 - UIScreenPixel)
if value == "" {
offset.x += UIScreenPixel
offset.y += 1.0 - UIScreenPixel
}
let valuePath = CGMutablePath()
valuePath.addRect(bounds.offsetBy(dx: offset.x, dy: offset.y))
let valueFramesetter = CTFramesetterCreateWithAttributedString(valueString as CFAttributedString)
let valyeFrame = CTFramesetterCreateFrame(valueFramesetter, CFRangeMake(0, valueString.length), valuePath, nil)
CTFrameDraw(valyeFrame, context)
})?.withRenderingMode(.alwaysTemplate)
}
let icon = generateIcon(value: timeoutValue)
let timeoutButtonSize = self.timeoutButton.update(
transition: transition,
component: AnyComponent(Button(
content: AnyComponent(Image(image: icon, tintColor: component.timeoutSelected ? UIColor(rgb: 0xf8d74a) : UIColor(white: 1.0, alpha: 1.0), size: CGSize(width: 20.0, height: 20.0))),
action: { [weak self] in
guard let self, let timeoutButtonView = self.timeoutButton.view else {
return
}
timeoutAction(timeoutButtonView)
component: AnyComponent(ContextReferenceButtonComponent(
content: AnyComponent(
TimeoutContentComponent(
color: .white,
accentColor: accentColor,
isSelected: component.timeoutSelected,
value: timeoutValue
)
),
tag: timeoutButtonTag,
minSize: CGSize(width: 32.0, height: 32.0),
action: { view, gesture in
timeoutAction(view, gesture)
}
).minSize(CGSize(width: 32.0, height: 32.0))),
)),
environment: {},
containerSize: CGSize(width: 32.0, height: 32.0)
)
@ -1296,7 +1507,9 @@ public final class MessageInputPanelComponent: Component {
}
var fieldBackgroundIsDark = false
if self.textFieldExternalState.hasText && component.alwaysDarkWhenHasText {
if component.style == .media {
} else if self.textFieldExternalState.hasText && component.alwaysDarkWhenHasText {
fieldBackgroundIsDark = true
} else if isEditing || component.style == .editor {
fieldBackgroundIsDark = true
@ -1468,8 +1681,12 @@ public final class MessageInputPanelComponent: Component {
self.updateContextQueries()
let panelLeftInset: CGFloat = max(insets.left, 7.0)
let panelRightInset: CGFloat = max(insets.right, 41.0)
var panelLeftInset: CGFloat = max(insets.left, 7.0)
var panelRightInset: CGFloat = max(insets.right, 41.0)
if case .media = component.style {
panelLeftInset = 0.0
panelRightInset = 0.0
}
var contextResults: ContextResultPanelComponent.Results?
if let result = self.contextQueryResults[.mention], case let .mentions(mentions) = result, !mentions.isEmpty {
@ -1601,7 +1818,13 @@ public final class MessageInputPanelComponent: Component {
containerSize: CGSize(width: availableSize.width - panelLeftInset - panelRightInset, height: availablePanelHeight)
)
let panelFrame = CGRect(origin: CGPoint(x: insets.left, y: -panelSize.height + 14.0), size: CGSize(width: panelSize.width, height: panelSize.height + 19.0))
var panelOriginY = -panelSize.height + 14.0
var panelHeight = panelSize.height + 19.0
if case .media = component.style {
panelOriginY -= 6.0
panelHeight = panelSize.height
}
let panelFrame = CGRect(origin: CGPoint(x: panelLeftInset, y: panelOriginY), size: CGSize(width: panelSize.width, height: panelHeight))
if let panelView = panel.view as? ContextResultPanelComponent.View {
if panelView.superview == nil {
self.insertSubview(panelView, at: 0)

View file

@ -0,0 +1,137 @@
import Foundation
import UIKit
import Display
import ComponentFlow
public final class TimeoutContentComponent: Component {
public let color: UIColor
public let accentColor: UIColor
public let isSelected: Bool
public let value: String
public init(
color: UIColor,
accentColor: UIColor,
isSelected: Bool,
value: String
) {
self.color = color
self.accentColor = accentColor
self.isSelected = isSelected
self.value = value
}
public static func ==(lhs: TimeoutContentComponent, rhs: TimeoutContentComponent) -> Bool {
if lhs.color != rhs.color {
return false
}
if lhs.accentColor != rhs.accentColor {
return false
}
if lhs.isSelected != rhs.isSelected {
return false
}
if lhs.value != rhs.value {
return false
}
return true
}
public final class View: UIView {
private var component: TimeoutContentComponent?
private weak var state: EmptyComponentState?
private let background: UIImageView
private let foreground: UIImageView
private let text = ComponentView<Empty>()
override init(frame: CGRect) {
self.background = UIImageView(image: UIImage(bundleImageName: "Media Editor/Timeout"))
self.foreground = UIImageView()
super.init(frame: frame)
self.addSubview(self.background)
self.addSubview(self.foreground)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: TimeoutContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let previousComponent = self.component
self.component = component
self.state = state
let size = CGSize(width: 20.0, height: 20.0)
if previousComponent?.accentColor != component.accentColor {
self.foreground.image = generateFilledCircleImage(diameter: size.width, color: component.accentColor)
}
var updated = false
if let previousComponent {
if previousComponent.isSelected != component.isSelected {
updated = true
}
if previousComponent.value != component.value {
if let textView = self.text.view, let snapshotView = textView.snapshotView(afterScreenUpdates: false) {
snapshotView.frame = textView.frame
self.addSubview(snapshotView)
snapshotView.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -3.0), duration: 0.2, removeOnCompletion: false, additive: true)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
snapshotView.removeFromSuperview()
})
textView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
textView.layer.animatePosition(from: CGPoint(x: 0.0, y: 3.0), to: .zero, duration: 0.2, additive: true)
}
}
}
let fontSize: CGFloat
let textOffset: CGFloat
if component.value.count == 1 {
fontSize = 12.0
textOffset = UIScreenPixel
} else {
fontSize = 10.0
textOffset = -UIScreenPixel
}
let font = Font.with(size: fontSize, design: .round, weight: .semibold)
let textSize = self.text.update(
transition: .immediate,
component: AnyComponent(Text(text: component.value, font: font, color: .white)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
if let textView = self.text.view {
if textView.superview == nil {
self.addSubview(textView)
}
let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0) + UIScreenPixel, y: floorToScreenPixels((size.height - textSize.height) / 2.0) + textOffset), size: textSize)
transition.setPosition(view: textView, position: textFrame.center)
textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size)
}
self.background.frame = CGRect(origin: .zero, size: size)
self.foreground.bounds = CGRect(origin: .zero, size: size)
self.foreground.center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
let foregroundTransition: Transition = updated ? .easeInOut(duration: 0.2) : transition
foregroundTransition.setScale(view: self.foreground, scale: component.isSelected ? 1.0 : 0.001)
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View file

@ -105,7 +105,7 @@ private final class NewSessionInfoSheetContentComponent: Component {
if self.remainingTimer > 0 {
buttonContents.append(AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(
AnimatedTextComponent(font: Font.with(size: 17.0, weight: .semibold, traits: .monospacedNumbers), color: environment.theme.list.itemCheckColors.foregroundColor.withMultipliedAlpha(0.5), items: [
AnimatedTextComponent.Item(id: AnyHashable(0 as Int), content: .number(self.remainingTimer))
AnimatedTextComponent.Item(id: AnyHashable(0 as Int), content: .number(self.remainingTimer, minDigits: 0))
])
)))
}

View file

@ -2296,7 +2296,7 @@ public final class StoryItemSetContainerComponent: Component {
if let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View {
Queue.mainQueue().justDispatch {
inputPanelView.clearSendMessageInput()
inputPanelView.clearSendMessageInput(updateState: true)
}
}
@ -2457,10 +2457,11 @@ public final class StoryItemSetContainerComponent: Component {
theme: component.theme,
strings: component.strings,
style: .story,
placeholder: inputPlaceholder,
placeholder: .plain(inputPlaceholder),
maxLength: 4096,
queryTypes: [.mention, .emoji],
alwaysDarkWhenHasText: component.metrics.widthClass == .regular,
resetInputContents: nil,
nextInputMode: { [weak self] hasText in
if case .media = self?.sendMessageContext.currentInputMode {
return .text
@ -2652,7 +2653,9 @@ public final class StoryItemSetContainerComponent: Component {
hideKeyboard: self.sendMessageContext.currentInputMode == .media,
forceIsEditing: self.sendMessageContext.currentInputMode == .media,
disabledPlaceholder: disabledPlaceholder,
storyId: component.slice.item.storyItem.id
isChannel: false,
storyItem: component.slice.item.storyItem,
chatLocation: nil
)),
environment: {},
containerSize: CGSize(width: inputPanelAvailableWidth, height: 200.0)

View file

@ -601,7 +601,7 @@ final class StoryItemSetContainerSendMessage {
}
}
})
inputPanelView.clearSendMessageInput()
inputPanelView.clearSendMessageInput(updateState: true)
self.currentInputMode = .text
if hasFirstResponder(view) {
@ -1259,7 +1259,7 @@ final class StoryItemSetContainerSendMessage {
guard let inputPanelView = view.inputPanel.view as? MessageInputPanelComponent.View else {
return
}
inputPanelView.clearSendMessageInput()
inputPanelView.clearSendMessageInput(updateState: true)
}
enum AttachMenuSubject {

View file

@ -90,6 +90,8 @@ public final class TextFieldComponent: Component {
public let textColor: UIColor
public let insets: UIEdgeInsets
public let hideKeyboard: Bool
public let resetText: NSAttributedString?
public let isOneLineWhenUnfocused: Bool
public let formatMenuAvailability: FormatMenuAvailability
public let lockedFormatAction: () -> Void
public let present: (ViewController) -> Void
@ -103,6 +105,8 @@ public final class TextFieldComponent: Component {
textColor: UIColor,
insets: UIEdgeInsets,
hideKeyboard: Bool,
resetText: NSAttributedString?,
isOneLineWhenUnfocused: Bool,
formatMenuAvailability: FormatMenuAvailability,
lockedFormatAction: @escaping () -> Void,
present: @escaping (ViewController) -> Void,
@ -115,6 +119,8 @@ public final class TextFieldComponent: Component {
self.textColor = textColor
self.insets = insets
self.hideKeyboard = hideKeyboard
self.resetText = resetText
self.isOneLineWhenUnfocused = isOneLineWhenUnfocused
self.formatMenuAvailability = formatMenuAvailability
self.lockedFormatAction = lockedFormatAction
self.present = present
@ -140,6 +146,12 @@ public final class TextFieldComponent: Component {
if lhs.hideKeyboard != rhs.hideKeyboard {
return false
}
if lhs.resetText != rhs.resetText {
return false
}
if lhs.isOneLineWhenUnfocused != rhs.isOneLineWhenUnfocused {
return false
}
if lhs.formatMenuAvailability != rhs.formatMenuAvailability {
return false
}
@ -189,6 +201,8 @@ public final class TextFieldComponent: Component {
private var customEmojiContainerView: CustomEmojiContainerView?
private var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)?
private let ellipsisView = ComponentView<Empty>()
private var inputState: InputState {
let selectionRange: Range<Int> = self.textView.selectedRange.location ..< (self.textView.selectedRange.location + self.textView.selectedRange.length)
return InputState(inputText: stateAttributedStringForText(self.textView.attributedText ?? NSAttributedString()), selectionRange: selectionRange)
@ -229,6 +243,10 @@ public final class TextFieldComponent: Component {
self.textContainer.widthTracksTextView = false
self.textContainer.heightTracksTextView = false
if #available(iOS 13.0, *) {
self.textView.overrideUserInterfaceStyle = .dark
}
self.textView.typingAttributes = [
NSAttributedString.Key.font: Font.regular(17.0),
NSAttributedString.Key.foregroundColor: UIColor.white
@ -258,6 +276,10 @@ public final class TextFieldComponent: Component {
self.updateEntities()
}
public func hasFirstResponder() -> Bool {
return self.textView.isFirstResponder
}
public func insertText(_ text: NSAttributedString) {
self.updateInputState { state in
return state.insertText(text)
@ -523,11 +545,13 @@ public final class TextFieldComponent: Component {
return self.inputState.inputText
}
public func setAttributedText(_ string: NSAttributedString) {
public func setAttributedText(_ string: NSAttributedString, updateState: Bool) {
self.updateInputState { _ in
return TextFieldComponent.InputState(inputText: string, selectionRange: string.length ..< string.length)
}
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged)))
if updateState {
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(AnimationHint(kind: .textChanged)))
}
}
public func activateInput() {
@ -774,6 +798,30 @@ public final class TextFieldComponent: Component {
component.externalState.hasTrackingView = hasTrackingView
}
func rightmostPositionOfFirstLine() -> CGPoint? {
let glyphRange = self.layoutManager.glyphRange(for: self.textContainer)
if glyphRange.length == 0 { return nil }
var lineRect = CGRect.zero
var glyphIndexForStringStart = glyphRange.location
var lineRange: NSRange = NSRange()
repeat {
lineRect = self.layoutManager.lineFragmentUsedRect(forGlyphAt: glyphIndexForStringStart, effectiveRange: &lineRange)
if NSMaxRange(lineRange) > glyphRange.length {
lineRange.length = glyphRange.length - lineRange.location
}
glyphIndexForStringStart = NSMaxRange(lineRange)
} while glyphIndexForStringStart < NSMaxRange(glyphRange) && !NSLocationInRange(glyphRange.location, lineRange)
let padding = self.textView.textContainerInset.left
let rightmostX = lineRect.maxX + padding
let rightmostY = lineRect.minY + self.textView.textContainerInset.top
return CGPoint(x: rightmostX, y: rightmostY)
}
func update(component: TextFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
self.state = state
@ -783,6 +831,10 @@ public final class TextFieldComponent: Component {
self.updateInputState { _ in
return TextFieldComponent.InputState(inputText: initialText)
}
} else if let resetText = component.resetText {
self.updateInputState { _ in
return TextFieldComponent.InputState(inputText: resetText)
}
}
if self.emojiViewProvider == nil {
@ -807,7 +859,10 @@ public final class TextFieldComponent: Component {
let wasEditing = component.externalState.isEditing
let isEditing = self.textView.isFirstResponder
let refreshScrolling = self.textView.bounds.size != size
var refreshScrolling = self.textView.bounds.size != size
if component.isOneLineWhenUnfocused && !isEditing && isEditing != wasEditing {
refreshScrolling = true
}
self.textView.frame = CGRect(origin: CGPoint(), size: size)
self.textView.panGestureRecognizer.isEnabled = isEditing
@ -815,7 +870,7 @@ public final class TextFieldComponent: Component {
if refreshScrolling {
if isEditing {
if wasEditing {
if wasEditing || component.isOneLineWhenUnfocused {
self.textView.setContentOffset(CGPoint(x: 0.0, y: max(0.0, self.textView.contentSize.height - self.textView.bounds.height)), animated: false)
}
} else {
@ -843,6 +898,43 @@ public final class TextFieldComponent: Component {
}
}
if component.isOneLineWhenUnfocused, let position = self.rightmostPositionOfFirstLine() {
let ellipsisSize = self.ellipsisView.update(
transition: transition,
component: AnyComponent(
Text(
text: "\u{2026}",
font: Font.regular(component.fontSize),
color: component.textColor
)
),
environment: {},
containerSize: availableSize
)
if let view = self.ellipsisView.view {
if view.superview == nil {
view.alpha = 0.0
self.textView.addSubview(view)
}
let ellipsisFrame = CGRect(origin: CGPoint(x: position.x - 11.0, y: position.y), size: ellipsisSize)
transition.setFrame(view: view, frame: ellipsisFrame)
let hasMoreThanOneLine = ellipsisFrame.maxY < self.textView.contentSize.height - 12.0
let ellipsisTransition: Transition
if isEditing {
ellipsisTransition = .easeInOut(duration: 0.2)
} else {
ellipsisTransition = .easeInOut(duration: 0.3)
}
ellipsisTransition.setAlpha(view: view, alpha: isEditing || !hasMoreThanOneLine ? 0.0 : 1.0)
}
} else {
if let view = self.ellipsisView.view {
view.removeFromSuperview()
}
}
self.updateEntities()
return size

View file

@ -1,22 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "SecretMediaIcon@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "SecretMediaIcon@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 753 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View file

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

View file

@ -0,0 +1,107 @@
%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.000000 1.669922 cm
0.000000 0.000000 0.000000 scn
4.000000 8.665078 m
4.367270 8.665078 4.665000 8.962809 4.665000 9.330078 c
4.665000 9.697348 4.367270 9.995078 4.000000 9.995078 c
4.000000 8.665078 l
h
8.665000 5.330078 m
8.665000 5.697348 8.367270 5.995078 8.000000 5.995078 c
7.632730 5.995078 7.335000 5.697348 7.335000 5.330078 c
8.665000 5.330078 l
h
4.000000 9.995078 m
1.423591 9.995078 -0.665000 7.906487 -0.665000 5.330078 c
0.665000 5.330078 l
0.665000 7.171948 2.158130 8.665078 4.000000 8.665078 c
4.000000 9.995078 l
h
-0.665000 5.330078 m
-0.665000 2.753670 1.423591 0.665078 4.000000 0.665078 c
4.000000 1.995078 l
2.158130 1.995078 0.665000 3.488208 0.665000 5.330078 c
-0.665000 5.330078 l
h
4.000000 0.665078 m
6.576408 0.665078 8.665000 2.753670 8.665000 5.330078 c
7.335000 5.330078 l
7.335000 3.488208 5.841870 1.995078 4.000000 1.995078 c
4.000000 0.665078 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 6.000000 8.705566 cm
0.000000 0.000000 0.000000 scn
0.800000 4.694434 m
0.470382 4.941647 0.000000 4.706456 0.000000 4.294434 c
0.000000 0.294434 l
0.000000 -0.117589 0.470382 -0.352780 0.800000 -0.105567 c
3.466667 1.894433 l
3.733333 2.094434 3.733333 2.494434 3.466667 2.694434 c
0.800000 4.694434 l
h
f*
n
Q
endstream
endobj
3 0 obj
1311
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 12.000000 14.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
0000001401 00000 n
0000001424 00000 n
0000001597 00000 n
0000001671 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1730
%%EOF

View file

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

View file

@ -0,0 +1,73 @@
%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.000000 2.780273 cm
0.000000 0.000000 0.000000 scn
2.332051 -0.225573 m
7.127887 2.971651 l
8.018486 3.565384 8.018486 4.874069 7.127886 5.467803 c
2.332050 8.665027 l
1.335218 9.329581 0.000000 8.614994 0.000000 7.416951 c
0.000000 1.022502 l
0.000000 -0.175541 1.335219 -0.890127 2.332051 -0.225573 c
h
f
n
Q
endstream
endobj
3 0 obj
379
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 12.000000 14.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
0000000469 00000 n
0000000491 00000 n
0000000664 00000 n
0000000738 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
797
%%EOF

View file

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

View file

@ -0,0 +1,212 @@
%PDF-1.7
1 0 obj
<< /ExtGState << /E4 << /ca 0.200000 >>
/E2 << /ca 0.600000 >>
/E3 << /ca 0.400000 >>
/E1 << /ca 0.800000 >>
>> >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 5.000000 3.670105 cm
0.000000 0.000000 0.000000 scn
10.000000 0.664955 m
10.367270 0.664955 10.665000 0.962687 10.665000 1.329956 c
10.665000 1.697226 10.367270 1.994957 10.000000 1.994957 c
10.000000 0.664955 l
h
10.000000 20.664955 m
10.367270 20.664955 10.665000 20.962687 10.665000 21.329956 c
10.665000 21.697226 10.367270 21.994957 10.000000 21.994957 c
10.000000 20.664955 l
h
10.000000 1.994957 m
4.844422 1.994957 0.665000 6.174377 0.665000 11.329956 c
-0.665000 11.329956 l
-0.665000 5.439838 4.109883 0.664955 10.000000 0.664955 c
10.000000 1.994957 l
h
0.665000 11.329956 m
0.665000 16.485535 4.844422 20.664955 10.000000 20.664955 c
10.000000 21.994957 l
4.109883 21.994957 -0.665000 17.220074 -0.665000 11.329956 c
0.665000 11.329956 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 13.462646 4.335022 cm
0.000000 0.000000 0.000000 scn
0.607792 21.290033 m
0.914249 21.316515 1.224271 21.330017 1.537287 21.330017 c
1.850302 21.330017 2.160324 21.316515 2.466781 21.290033 c
2.832687 21.258417 3.103682 20.936161 3.072065 20.570255 c
3.040448 20.204350 2.718192 19.933353 2.352286 19.964972 c
2.083856 19.988165 1.812035 20.000017 1.537287 20.000017 c
1.262538 20.000017 0.990717 19.988165 0.722287 19.964972 c
0.356381 19.933353 0.034125 20.204350 0.002508 20.570255 c
-0.029109 20.936161 0.241886 21.258417 0.607792 21.290033 c
h
0.002508 0.759779 m
0.034125 1.125685 0.356381 1.396679 0.722287 1.365063 c
0.990717 1.341867 1.262538 1.330017 1.537287 1.330017 c
1.812035 1.330017 2.083856 1.341867 2.352286 1.365063 c
2.718192 1.396679 3.040448 1.125685 3.072065 0.759779 c
3.103682 0.393873 2.832687 0.071617 2.466781 0.039999 c
2.160324 0.013519 1.850302 0.000015 1.537287 0.000015 c
1.224271 0.000015 0.914249 0.013519 0.607792 0.039999 c
0.241886 0.071617 -0.029109 0.393873 0.002508 0.759779 c
h
f*
n
Q
q
1.000000 0.000000 -0.000000 1.000000 18.562012 5.086609 cm
0.000000 0.000000 0.000000 scn
2.718522 2.103071 m
2.929436 1.802403 2.856679 1.387682 2.556010 1.176766 c
2.050149 0.821909 1.511930 0.509789 0.946597 0.245714 c
0.613840 0.090281 0.218083 0.234030 0.062649 0.566786 c
-0.092785 0.899544 0.050963 1.295300 0.383720 1.450733 c
0.878228 1.681723 1.349272 1.954861 1.792216 2.265581 c
2.092885 2.476498 2.507605 2.403738 2.718522 2.103071 c
h
f*
n
Q
q
/E1 gs
1.000000 0.000000 -0.000000 1.000000 22.527100 8.415771 cm
0.000000 0.000000 0.000000 scn
1.819443 2.959568 m
2.152200 2.804134 2.295949 2.408377 2.140515 2.075620 c
1.876443 1.510287 1.564322 0.972069 1.209464 0.466206 c
0.998547 0.165539 0.583827 0.092780 0.283160 0.303696 c
-0.017509 0.514613 -0.090267 0.929333 0.120648 1.230002 c
0.431370 1.672945 0.704507 2.143989 0.935496 2.638497 c
1.090930 2.971253 1.486687 3.115002 1.819443 2.959568 c
h
f*
n
Q
q
/E2 gs
1.000000 0.000000 -0.000000 1.000000 24.297363 13.404541 cm
0.000000 0.000000 0.000000 scn
0.607792 3.130305 m
0.973697 3.161922 1.295953 2.890927 1.327572 2.525021 c
1.354051 2.218563 1.367555 1.908542 1.367555 1.595526 c
1.367555 1.282510 1.354051 0.972489 1.327572 0.666031 c
1.295953 0.300125 0.973697 0.029130 0.607792 0.060747 c
0.241886 0.092365 -0.029108 0.414621 0.002508 0.780526 c
0.025703 1.048956 0.037553 1.320777 0.037553 1.595526 c
0.037553 1.870275 0.025703 2.142096 0.002508 2.410526 c
-0.029108 2.776431 0.241886 3.098687 0.607792 3.130305 c
h
f*
n
Q
q
/E3 gs
1.000000 0.000000 -0.000000 1.000000 22.527100 18.379028 cm
0.000000 0.000000 0.000000 scn
0.283159 2.901568 m
0.583828 3.112484 0.998548 3.039725 1.209465 2.739057 c
1.564321 2.233195 1.876442 1.694978 2.140516 1.129645 c
2.295950 0.796888 2.152200 0.401131 1.819444 0.245697 c
1.486686 0.090263 1.090931 0.234012 0.935497 0.566768 c
0.704508 1.061275 0.431369 1.532320 0.120649 1.975264 c
-0.090267 2.275931 -0.017508 2.690652 0.283159 2.901568 c
h
f*
n
Q
q
/E4 gs
1.000000 0.000000 -0.000000 1.000000 18.562012 22.344177 cm
0.000000 0.000000 0.000000 scn
0.062649 2.002510 m
0.218083 2.335267 0.613840 2.479015 0.946597 2.323581 c
1.511930 2.059509 2.050148 1.747388 2.556011 1.392531 c
2.856678 1.181615 2.929437 0.766894 2.718521 0.466226 c
2.507604 0.165558 2.092884 0.092800 1.792215 0.303715 c
1.349271 0.614436 0.878228 0.887573 0.383720 1.118563 c
0.050964 1.273997 -0.092785 1.669754 0.062649 2.002510 c
h
f*
n
Q
q
1.000000 0.000000 -0.000000 1.000000 12.500000 9.737305 cm
0.000000 0.000000 0.000000 scn
2.718506 0.000000 m
2.324219 0.000000 2.075195 0.290527 2.075195 0.715942 c
2.075195 8.443481 l
1.099854 7.727539 l
0.913086 7.571899 0.809326 7.530396 0.601807 7.530396 c
0.269775 7.530396 0.000000 7.820923 0.000000 8.163330 c
0.000000 8.401978 0.114136 8.588745 0.352783 8.765137 c
1.774292 9.730103 l
2.147827 9.979126 2.355347 10.072510 2.645874 10.072510 c
3.112793 10.072510 3.372192 9.792358 3.372192 9.263184 c
3.372192 0.715942 l
3.372192 0.280151 3.123169 0.000000 2.718506 0.000000 c
h
f
n
Q
endstream
endobj
3 0 obj
4922
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
0000000216 00000 n
0000005194 00000 n
0000005217 00000 n
0000005390 00000 n
0000005464 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
5523
%%EOF

View file

@ -0,0 +1,163 @@
%PDF-1.7
1 0 obj
<< /Length 2 0 R
/Range [ 0.000000 1.000000 0.000000 1.000000 0.000000 1.000000 ]
/Domain [ 0.000000 1.000000 ]
/FunctionType 4
>>
stream
{ 0.419608 exch 0.576471 exch 1.000000 exch dup 0.000000 gt { exch pop exch pop exch pop dup 0.000000 sub 0.392998 mul 0.419608 add exch dup 0.000000 sub -0.321544 mul 0.576471 add exch dup 0.000000 sub 0.000000 mul 1.000000 add exch } if dup 0.439058 gt { exch pop exch pop exch pop dup 0.439058 sub 0.538311 mul 0.592157 add exch dup 0.439058 sub -0.034955 mul 0.435294 add exch dup 0.439058 sub -0.342561 mul 1.000000 add exch } if dup 1.000000 gt { exch pop exch pop exch pop 0.894118 exch 0.415686 exch 0.807843 exch } if pop }
endstream
endobj
2 0 obj
533
endobj
3 0 obj
<< /Pattern << /P1 << /Matrix [ 105.698799 22.310228 -22.310228 105.698799 -4.867018 -86.125580 ]
/Shading << /Coords [ 0.000000 0.000000 1.000000 0.000000 ]
/ColorSpace /DeviceRGB
/Function 1 0 R
/Domain [ 0.000000 1.000000 ]
/ShadingType 2
/Extend [ true true ]
>>
/PatternType 2
/Type /Pattern
>> >> >>
endobj
4 0 obj
<< /Length 5 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
q
1.000000 0.000000 -0.000000 1.000000 1.659973 -1.660000 cm
0.949020 0.949020 0.968627 scn
22.340000 15.320000 m
22.340000 9.609375 17.710625 4.980000 12.000000 4.980000 c
12.000000 1.660000 l
19.544210 1.660000 25.660000 7.775789 25.660000 15.320000 c
22.340000 15.320000 l
h
12.000000 4.980000 m
6.289376 4.980000 1.660000 9.609375 1.660000 15.320000 c
-1.660000 15.320000 l
-1.660000 7.775789 4.455791 1.660000 12.000000 1.660000 c
12.000000 4.980000 l
h
1.660000 15.320000 m
1.660000 21.030624 6.289376 25.660000 12.000000 25.660000 c
12.000000 28.980000 l
4.455791 28.980000 -1.660000 22.864208 -1.660000 15.320000 c
1.660000 15.320000 l
h
12.000000 25.660000 m
17.710625 25.660000 22.340000 21.030624 22.340000 15.320000 c
25.660000 15.320000 l
25.660000 22.864208 19.544210 28.980000 12.000000 28.980000 c
12.000000 25.660000 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 1.659973 -1.660000 cm
0.850980 0.850980 0.850980 scn
24.000000 15.320000 m
24.000000 8.692583 18.627417 3.320000 12.000000 3.320000 c
5.372583 3.320000 0.000000 8.692583 0.000000 15.320000 c
0.000000 21.947416 5.372583 27.320000 12.000000 27.320000 c
18.627417 27.320000 24.000000 21.947416 24.000000 15.320000 c
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 1.659973 -1.660000 cm
/Pattern cs
/P1 scn
24.000000 15.320000 m
24.000000 8.692583 18.627417 3.320000 12.000000 3.320000 c
5.372583 3.320000 0.000000 8.692583 0.000000 15.320000 c
0.000000 21.947416 5.372583 27.320000 12.000000 27.320000 c
18.627417 27.320000 24.000000 21.947416 24.000000 15.320000 c
h
f
n
Q
Q
q
1.000000 0.000000 -0.000000 1.000000 8.793701 5.165123 cm
1.000000 1.000000 1.000000 scn
6.639333 9.995974 m
6.270643 9.995974 5.989172 10.325375 6.046674 10.689552 c
6.848394 15.767110 l
6.948168 16.399014 6.121798 16.727470 5.760551 16.199497 c
0.105843 7.934922 l
-0.166615 7.536714 0.118531 6.996113 0.601028 6.996113 c
3.087691 6.996113 l
3.456380 6.996113 3.737850 6.666713 3.680349 6.302535 c
2.878629 1.224977 l
2.778855 0.593074 3.605225 0.264616 3.966471 0.792590 c
9.621180 9.057164 l
9.893639 9.455373 9.608493 9.995974 9.125995 9.995974 c
6.639333 9.995974 l
h
f*
n
Q
endstream
endobj
5 0 obj
2168
endobj
6 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 27.319946 27.320000 ]
/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
0000000727 00000 n
0000000749 00000 n
0000001379 00000 n
0000003603 00000 n
0000003626 00000 n
0000003799 00000 n
0000003873 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 8 0 R
/Size 9
>>
startxref
3932
%%EOF

View file

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

View file

@ -0,0 +1,79 @@
%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 1.889648 0.842155 cm
1.000000 1.000000 1.000000 scn
11.065588 16.659954 m
10.451106 16.659954 9.981988 17.208954 10.077824 17.815918 c
11.413990 26.278299 l
11.580280 27.331470 10.202996 27.878902 9.600920 26.998945 c
0.176406 13.224656 l
-0.277692 12.560975 0.197552 11.659972 1.001714 11.659972 c
5.146065 11.659972 l
5.760547 11.659972 6.229665 11.110973 6.133829 10.504009 c
4.797663 2.041628 l
4.631372 0.988457 6.008657 0.441025 6.610733 1.320982 c
16.035248 15.095271 l
16.489344 15.758952 16.014101 16.659954 15.209939 16.659954 c
11.065588 16.659954 l
h
f*
n
Q
endstream
endobj
3 0 obj
637
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 20.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
0000000727 00000 n
0000000749 00000 n
0000000922 00000 n
0000000996 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1055
%%EOF

View file

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

View file

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

View file

@ -0,0 +1,79 @@
%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 1.259766 0.561462 cm
1.000000 1.000000 1.000000 scn
7.377057 11.106611 m
6.967402 11.106611 6.654656 11.472610 6.718547 11.877253 c
7.609325 17.518847 l
7.720185 18.220961 6.801996 18.585918 6.400612 17.999279 c
0.117604 8.816422 l
-0.185128 8.373968 0.131700 7.773299 0.667809 7.773299 c
3.430712 7.773299 l
3.840366 7.773299 4.153112 7.407299 4.089221 7.002657 c
3.198443 1.361063 l
3.087583 0.658949 4.005772 0.293991 4.407156 0.880630 c
10.690165 10.063488 l
10.992896 10.505941 10.676067 11.106611 10.139959 11.106611 c
7.377057 11.106611 l
h
f*
n
Q
endstream
endobj
3 0 obj
622
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 13.333374 20.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
0000000712 00000 n
0000000734 00000 n
0000000907 00000 n
0000000981 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1040
%%EOF

View file

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

View file

@ -0,0 +1,402 @@
%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 9.000000 7.000000 cm
1.000000 1.000000 1.000000 scn
1.092019 13.782013 m
1.519843 14.000000 2.079895 14.000000 3.200000 14.000000 c
3.674516 14.000000 l
4.163698 14.000000 4.408288 14.000000 4.638463 13.944740 c
4.842536 13.895746 5.037625 13.814938 5.216570 13.705280 c
5.418403 13.581597 5.591354 13.408646 5.937256 13.062744 c
5.937259 13.062741 l
10.062741 8.937258 l
10.408645 8.591354 10.581596 8.418404 10.705280 8.216570 c
10.814938 8.037625 10.895746 7.842536 10.944740 7.638463 c
11.000000 7.408288 11.000000 7.163698 11.000000 6.674517 c
11.000000 3.200000 l
11.000000 2.079895 11.000000 1.519842 10.782013 1.092019 c
10.590266 0.715694 10.284306 0.409734 9.907981 0.217987 c
9.480158 0.000000 8.920105 0.000000 7.800001 0.000000 c
3.200000 0.000000 l
2.079895 0.000000 1.519843 0.000000 1.092019 0.217987 c
0.715695 0.409734 0.409734 0.715694 0.217987 1.092019 c
0.000000 1.519842 0.000000 2.079895 0.000000 3.200000 c
0.000000 10.800000 l
0.000000 11.920105 0.000000 12.480158 0.217987 12.907981 c
0.409734 13.284306 0.715695 13.590266 1.092019 13.782013 c
h
5.000000 11.896446 m
5.000000 9.000000 l
5.000000 8.447716 5.447715 8.000000 6.000000 8.000000 c
8.896446 8.000000 l
9.119173 8.000000 9.230715 8.269285 9.073223 8.426777 c
5.426776 12.073224 l
5.269285 12.230715 5.000000 12.119173 5.000000 11.896446 c
h
f*
n
Q
q
q
1.000000 0.000000 -0.000000 1.000000 4.000000 0.330414 cm
0.529412 0.490196 1.000000 scn
1.092019 16.451599 m
0.488212 17.636639 l
0.488212 17.636639 l
1.092019 16.451599 l
h
4.638463 16.614326 m
4.327981 15.321074 l
4.327981 15.321074 l
4.638463 16.614326 l
h
5.216570 16.374866 m
4.521646 15.240855 l
4.521647 15.240855 l
5.216570 16.374866 l
h
5.937256 15.732330 m
6.931310 16.615934 l
6.914003 16.635405 6.896128 16.654364 6.877707 16.672783 c
5.937256 15.732330 l
h
5.937259 15.732327 m
4.943204 14.848723 l
4.960511 14.829253 4.978386 14.810296 4.996807 14.791876 c
5.937259 15.732327 l
h
10.062741 11.606844 m
9.122289 10.666392 l
9.122290 10.666392 l
10.062741 11.606844 l
h
10.705280 10.886156 m
11.839292 11.581079 l
11.839291 11.581080 l
10.705280 10.886156 l
h
10.944740 10.308050 m
12.237993 10.618530 l
12.237991 10.618536 l
10.944740 10.308050 l
h
10.782013 3.761605 m
9.596974 4.365414 l
9.596973 4.365412 l
10.782013 3.761605 l
h
9.907981 2.887573 m
9.304174 4.072613 l
9.304173 4.072612 l
9.907981 2.887573 l
h
1.092019 2.887573 m
1.695827 4.072612 l
1.695826 4.072612 l
1.092019 2.887573 l
h
0.217987 3.761605 m
1.403026 4.365412 l
1.403025 4.365413 l
0.217987 3.761605 l
h
0.217987 15.577567 m
-0.967052 16.181376 l
-0.967052 16.181376 l
0.217987 15.577567 l
h
9.073223 11.096363 m
8.132771 10.155910 l
8.132772 10.155910 l
9.073223 11.096363 l
h
5.426776 14.742810 m
6.367229 15.683262 l
6.367229 15.683262 l
5.426776 14.742810 l
h
3.200000 17.999586 m
2.661894 17.999586 2.178326 18.000620 1.778100 17.967920 c
1.362347 17.933952 0.920866 17.857086 0.488212 17.636639 c
1.695826 15.266561 l
1.696496 15.266901 1.699535 15.268437 1.706597 15.270958 c
1.713946 15.273581 1.727419 15.277899 1.749235 15.283036 c
1.795082 15.293834 1.870400 15.306599 1.994708 15.316755 c
2.261491 15.338552 2.618001 15.339586 3.200000 15.339586 c
3.200000 17.999586 l
h
3.674516 17.999586 m
3.200000 17.999586 l
3.200000 15.339586 l
3.674516 15.339586 l
3.674516 17.999586 l
h
4.948946 17.907578 m
4.530101 18.008133 4.100392 17.999586 3.674516 17.999586 c
3.674516 15.339586 l
4.227004 15.339586 4.286476 15.331038 4.327981 15.321074 c
4.948946 17.907578 l
h
5.911493 17.508879 m
5.613550 17.691458 5.288726 17.826004 4.948946 17.907578 c
4.327981 15.321074 l
4.396346 15.304661 4.461700 15.277590 4.521646 15.240855 c
5.911493 17.508879 l
h
6.877707 16.672783 m
6.576569 16.973921 6.278763 17.283815 5.911493 17.508879 c
4.521647 15.240855 l
4.558042 15.218552 4.606139 15.182544 4.996805 14.791878 c
6.877707 16.672783 l
h
6.931313 16.615932 m
6.931310 16.615934 l
4.943202 14.848727 l
4.943204 14.848723 l
6.931313 16.615932 l
h
11.003194 12.547297 m
6.877711 16.672779 l
4.996807 14.791876 l
9.122289 10.666392 l
11.003194 12.547297 l
h
11.839291 11.581080 m
11.614226 11.948351 11.304334 12.246157 11.003193 12.547297 c
9.122290 10.666392 l
9.512956 10.275725 9.548966 10.227628 9.571270 10.191232 c
11.839291 11.581080 l
h
12.237991 10.618536 m
12.156417 10.958313 12.021872 11.283136 11.839292 11.581079 c
9.571269 10.191234 l
9.608004 10.131287 9.635076 10.065931 9.651489 9.997564 c
12.237991 10.618536 l
h
12.330000 9.344103 m
12.330000 9.769979 12.338548 10.199686 12.237993 10.618530 c
9.651487 9.997570 l
9.661452 9.956062 9.670000 9.896589 9.670000 9.344103 c
12.330000 9.344103 l
h
12.330000 5.869586 m
12.330000 9.344103 l
9.670000 9.344103 l
9.670000 5.869586 l
12.330000 5.869586 l
h
11.967052 3.157797 m
12.187500 3.590451 12.264366 4.031933 12.298335 4.447685 c
12.331035 4.847912 12.330000 5.331480 12.330000 5.869586 c
9.670000 5.869586 l
9.670000 5.287587 9.668965 4.931077 9.647168 4.664294 c
9.637012 4.539987 9.624248 4.464668 9.613450 4.418821 c
9.608313 4.397006 9.603994 4.383533 9.601372 4.376184 c
9.598851 4.369123 9.597316 4.366082 9.596974 4.365414 c
11.967052 3.157797 l
h
10.511787 1.702534 m
11.138369 2.021792 11.647794 2.531218 11.967052 3.157799 c
9.596973 4.365412 l
9.532739 4.239344 9.430243 4.136847 9.304174 4.072613 c
10.511787 1.702534 l
h
7.800001 1.339586 m
8.338107 1.339586 8.821674 1.338552 9.221901 1.371251 c
9.637653 1.405220 10.079135 1.482086 10.511789 1.702535 c
9.304173 4.072612 l
9.303504 4.072270 9.300464 4.070735 9.293403 4.068214 c
9.286054 4.065592 9.272580 4.061274 9.250765 4.056136 c
9.204918 4.045339 9.129600 4.032574 9.005292 4.022418 c
8.738509 4.000621 8.381999 3.999586 7.800001 3.999586 c
7.800001 1.339586 l
h
3.200000 1.339586 m
7.800001 1.339586 l
7.800001 3.999586 l
3.200000 3.999586 l
3.200000 1.339586 l
h
0.488211 1.702535 m
0.920865 1.482086 1.362347 1.405220 1.778099 1.371251 c
2.178326 1.338552 2.661894 1.339586 3.200000 1.339586 c
3.200000 3.999586 l
2.618001 3.999586 2.261491 4.000621 1.994708 4.022418 c
1.870400 4.032574 1.795082 4.045339 1.749235 4.056136 c
1.727419 4.061274 1.713946 4.065592 1.706597 4.068214 c
1.699536 4.070735 1.696496 4.072271 1.695827 4.072612 c
0.488211 1.702535 l
h
-0.967052 3.157799 m
-0.647793 2.531218 -0.138368 2.021792 0.488212 1.702535 c
1.695826 4.072612 l
1.569757 4.136847 1.467261 4.239344 1.403026 4.365412 c
-0.967052 3.157799 l
h
-1.330000 5.869586 m
-1.330000 5.331480 -1.331034 4.847912 -1.298335 4.447685 c
-1.264366 4.031933 -1.187500 3.590451 -0.967052 3.157798 c
1.403025 4.365413 l
1.402685 4.366082 1.401149 4.369122 1.398628 4.376184 c
1.396005 4.383532 1.391687 4.397006 1.386550 4.418821 c
1.375753 4.464668 1.362988 4.539987 1.352831 4.664294 c
1.331034 4.931077 1.330000 5.287587 1.330000 5.869586 c
-1.330000 5.869586 l
h
-1.330000 13.469586 m
-1.330000 5.869586 l
1.330000 5.869586 l
1.330000 13.469586 l
-1.330000 13.469586 l
h
-0.967052 16.181376 m
-1.187500 15.748720 -1.264366 15.307240 -1.298335 14.891487 c
-1.331034 14.491261 -1.330000 14.007692 -1.330000 13.469586 c
1.330000 13.469586 l
1.330000 14.051584 1.331034 14.408095 1.352831 14.674878 c
1.362988 14.799186 1.375753 14.874504 1.386550 14.920351 c
1.391687 14.942167 1.396005 14.955641 1.398629 14.962990 c
1.401149 14.970051 1.402685 14.973091 1.403026 14.973760 c
-0.967052 16.181376 l
h
0.488212 17.636639 m
-0.138368 17.317379 -0.647793 16.807955 -0.967052 16.181376 c
1.403026 14.973760 l
1.467261 15.099829 1.569758 15.202326 1.695826 15.266561 c
0.488212 17.636639 l
h
6.330000 11.669586 m
6.330000 14.566032 l
3.670000 14.566032 l
3.670000 11.669586 l
6.330000 11.669586 l
h
6.000000 11.999586 m
6.182254 11.999586 6.330000 11.851840 6.330000 11.669586 c
3.670000 11.669586 l
3.670000 10.382763 4.713177 9.339586 6.000000 9.339586 c
6.000000 11.999586 l
h
8.896446 11.999586 m
6.000000 11.999586 l
6.000000 9.339586 l
8.896446 9.339586 l
8.896446 11.999586 l
h
8.132772 10.155910 m
7.452406 10.836273 7.934273 11.999586 8.896446 11.999586 c
8.896446 9.339586 l
10.304073 9.339586 11.009024 11.041470 10.013674 12.036817 c
8.132772 10.155910 l
h
4.486324 13.802358 m
8.132771 10.155910 l
10.013675 12.036816 l
6.367229 15.683262 l
4.486324 13.802358 l
h
6.330000 14.566032 m
6.330000 13.603863 5.166691 13.121991 4.486324 13.802358 c
6.367229 15.683262 l
5.371879 16.678612 3.670000 15.973656 3.670000 14.566032 c
6.330000 14.566032 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 4.000000 0.330414 cm
1.000000 1.000000 1.000000 scn
1.092019 16.451599 m
1.519843 16.669586 2.079895 16.669586 3.200000 16.669586 c
3.674516 16.669586 l
4.163698 16.669586 4.408288 16.669586 4.638463 16.614326 c
4.842536 16.565332 5.037625 16.484524 5.216570 16.374866 c
5.418403 16.251183 5.591354 16.078232 5.937256 15.732330 c
5.937259 15.732327 l
10.062741 11.606844 l
10.408645 11.260941 10.581596 11.087990 10.705280 10.886156 c
10.814938 10.707211 10.895746 10.512122 10.944740 10.308050 c
11.000000 10.077875 11.000000 9.833284 11.000000 9.344103 c
11.000000 5.869586 l
11.000000 4.749481 11.000000 4.189428 10.782013 3.761605 c
10.590266 3.385281 10.284306 3.079320 9.907981 2.887573 c
9.480158 2.669586 8.920105 2.669586 7.800001 2.669586 c
3.200000 2.669586 l
2.079895 2.669586 1.519843 2.669586 1.092019 2.887573 c
0.715695 3.079320 0.409734 3.385281 0.217987 3.761605 c
0.000000 4.189428 0.000000 4.749481 0.000000 5.869586 c
0.000000 13.469586 l
0.000000 14.589691 0.000000 15.149744 0.217987 15.577567 c
0.409734 15.953892 0.715695 16.259853 1.092019 16.451599 c
h
5.000000 14.566032 m
5.000000 11.669586 l
5.000000 11.117302 5.447715 10.669586 6.000000 10.669586 c
8.896446 10.669586 l
9.119173 10.669586 9.230715 10.938871 9.073223 11.096363 c
5.426776 14.742810 l
5.269285 14.900301 5.000000 14.788759 5.000000 14.566032 c
h
f*
n
Q
Q
endstream
endobj
3 0 obj
9865
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
0000009955 00000 n
0000009978 00000 n
0000010151 00000 n
0000010225 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
10284
%%EOF

View file

@ -243,6 +243,8 @@ public final class AccountContextImpl: AccountContext {
private var userLimitsConfigurationDisposable: Disposable?
public private(set) var userLimits: EngineConfiguration.UserLimits
public private(set) var isPremium: Bool
public init(sharedContext: SharedAccountContextImpl, account: Account, limitsConfiguration: LimitsConfiguration, contentSettings: ContentSettings, appConfiguration: AppConfiguration, temp: Bool = false)
{
self.sharedContextImpl = sharedContext
@ -250,6 +252,7 @@ public final class AccountContextImpl: AccountContext {
self.engine = TelegramEngine(account: account)
self.userLimits = EngineConfiguration.UserLimits(UserLimitsConfiguration.defaultValue)
self.isPremium = false
self.downloadedMediaStoreManager = DownloadedMediaStoreManagerImpl(postbox: account.postbox, accountManager: sharedContext.accountManager)
@ -380,14 +383,19 @@ public final class AccountContextImpl: AccountContext {
})
self.userLimitsConfigurationDisposable = (self.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: account.peerId))
|> mapToSignal { peer -> Signal<EngineConfiguration.UserLimits, NoError> in
return self.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: peer?.isPremium ?? false))
|> mapToSignal { peer -> Signal<(Bool, EngineConfiguration.UserLimits), NoError> in
let isPremium = peer?.isPremium ?? false
return self.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: isPremium))
|> map { userLimits in
return (isPremium, userLimits)
}
}
|> deliverOnMainQueue).start(next: { [weak self] value in
|> deliverOnMainQueue).start(next: { [weak self] isPremium, userLimits in
guard let strongSelf = self else {
return
}
strongSelf.userLimits = value
strongSelf.isPremium = isPremium
strongSelf.userLimits = userLimits
})
}
@ -399,6 +407,7 @@ public final class AccountContextImpl: AccountContext {
self.countriesConfigurationDisposable?.dispose()
self.experimentalUISettingsDisposable?.dispose()
self.animatedEmojiStickersDisposable?.dispose()
self.userLimitsConfigurationDisposable?.dispose()
}
public func storeSecureIdPassword(password: String) {

View file

@ -4162,7 +4162,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.chatDisplayNode.dismissInput()
let botName = EnginePeer(peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)
let botAddress = peer.addressName ?? ""
if source == .generic {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
return $0.updatedTitlePanelContext {
@ -4216,6 +4217,22 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let params = WebAppParameters(peerId: peerId, botId: peerId, botName: botName, url: url, queryId: nil, payload: nil, buttonText: buttonText, keepAliveSignal: nil, fromMenu: true, fromAttachMenu: false, isInline: false, isSimple: false)
let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, params: params, threadId: strongSelf.chatLocation.threadId, openUrl: { [weak self] url, concealed, commit in
self?.openUrl(url, concealed: concealed, forceExternal: true, commit: commit)
}, requestSwitchInline: { [weak self] query, chatTypes, completion in
if let strongSelf = self {
if let chatTypes {
let controller = strongSelf.context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: strongSelf.context, filter: [.excludeRecent, .doNotSearchMessages], requestPeerType: chatTypes, hasContactSelector: false, hasCreation: false))
controller.peerSelected = { [weak self, weak controller] peer, _ in
if let strongSelf = self {
completion()
controller?.dismiss()
strongSelf.controllerInteraction?.activateSwitchInline(peer.id, "@\(botAddress) \(query)", nil)
}
}
strongSelf.push(controller)
} else {
strongSelf.controllerInteraction?.activateSwitchInline(peerId, "@\(botAddress) \(query)", nil)
}
}
}, getInputContainerNode: { [weak self] in
if let strongSelf = self, let layout = strongSelf.validLayout, case .compact = layout.metrics.widthClass {
return (strongSelf.chatDisplayNode.getWindowInputAccessoryHeight(), strongSelf.chatDisplayNode.inputPanelContainerNode, {
@ -4260,7 +4277,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
botAddress = bot.addressName ?? ""
}
strongSelf.messageActionCallbackDisposable.set(((strongSelf.context.engine.messages.requestSimpleWebView(botId: botId, url: url, inline: isInline, themeParams: generateWebAppThemeParams(strongSelf.presentationData.theme))
strongSelf.messageActionCallbackDisposable.set(((strongSelf.context.engine.messages.requestSimpleWebView(botId: botId, url: url, source: isInline ? .inline : .generic, themeParams: generateWebAppThemeParams(strongSelf.presentationData.theme))
|> afterDisposed {
updateProgress()
})
@ -13098,7 +13115,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case bot(id: PeerId, payload: String?, justInstalled: Bool)
case gift
}
//editMediaOptions: MessageMediaEditingOptions?, editMediaReference: AnyMediaReference?, botId: PeerId? = nil, botPayload: String? = nil, botJustInstalled: Bool = false
private func presentAttachmentMenu(subject: AttachMenuSubject) {
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return
@ -13266,9 +13283,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} else {
let _ = (context.engine.messages.getAttachMenuBot(botId: botId)
|> deliverOnMainQueue).start(next: { bot in
let peer = EnginePeer(bot.peer)
let controller = addWebAppToAttachmentController(context: context, peerName: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), icons: bot.icons, requestWriteAccess: bot.flags.contains(.requiresWriteAccess), completion: { allowWrite in
let controller = addWebAppToAttachmentController(context: context, peerName: bot.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), icons: bot.icons, requestWriteAccess: bot.flags.contains(.requiresWriteAccess), completion: { allowWrite in
let _ = (context.engine.messages.addBotToAttachMenu(botId: botId, allowWrite: allowWrite)
|> deliverOnMainQueue).start(error: { _ in

View file

@ -542,10 +542,10 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
switch preparePosition {
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
if let count = webpageGalleryMediaCount {
additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: presentationData.strings.Items_NOfM("1", "\(count)").string))
additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: presentationData.strings.Items_NOfM("1", "\(count)").string), iconName: nil)
skipStandardStatus = isImage
} else if let mediaBadge = mediaBadge {
additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: mediaBadge))
additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: mediaBadge), iconName: nil)
} else {
skipStandardStatus = isFile
}

View file

@ -80,9 +80,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
private var automaticDownload: Bool?
var media: TelegramMediaFile?
var appliedForwardInfo: (Peer?, String?)?
private var secretProgressIcon: UIImage?
private let fetchDisposable = MetaDisposable()
private var durationBackgroundNode: NavigationBackgroundNode?
@ -259,9 +257,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
let theme = item.presentationData.theme
let isSecretMedia = item.message.containsSecretMedia
var secretProgressIcon: UIImage?
if isSecretMedia {
secretProgressIcon = PresentationResourcesChat.chatBubbleSecretMediaIcon(theme.theme)
secretVideoPlaceholderBackgroundImage = PresentationResourcesChat.chatInstantVideoBackgroundImage(theme.theme, wallpaper: !theme.wallpaper.isEmpty)
}
@ -575,7 +571,6 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
strongSelf.item = item
strongSelf.videoFrame = displayVideoFrame
strongSelf.appliedForwardInfo = (forwardSource, forwardAuthorSignature)
strongSelf.secretProgressIcon = secretProgressIcon
strongSelf.automaticDownload = automaticDownload
@ -1155,11 +1150,11 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
state = .progress(color: messageTheme.mediaOverlayControlColors.foregroundColor, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true, animateRotation: true)
}
case .Local:
if isSecretMedia && self.secretProgressIcon != nil {
if isSecretMedia {
if let (beginTime, timeout) = secretBeginTimeAndTimeout {
state = .secretTimeout(color: messageTheme.mediaOverlayControlColors.foregroundColor, icon: secretProgressIcon, beginTime: beginTime, timeout: timeout, sparks: true)
state = .secretTimeout(color: messageTheme.mediaOverlayControlColors.foregroundColor, icon: .flame, beginTime: beginTime, timeout: timeout, sparks: true)
} else {
state = .customIcon(secretProgressIcon!)
state = .staticTimeout
}
} else {
state = .none

View file

@ -189,7 +189,7 @@ private class ExtendedMediaOverlayNode: ASDisplayNode {
var isRevealed = false
var tapped: () -> Void = {}
init(enableAnimations: Bool) {
init(hasImageOverlay: Bool, enableAnimations: Bool) {
self.blurredImageNode = TransformImageNode()
self.blurredImageNode.contentAnimations = []
@ -212,7 +212,9 @@ private class ExtendedMediaOverlayNode: ASDisplayNode {
super.init()
self.addSubnode(self.blurredImageNode)
if hasImageOverlay {
self.addSubnode(self.blurredImageNode)
}
self.addSubnode(self.dustNode)
self.addSubnode(self.buttonNode)
@ -860,13 +862,10 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
let maxWidth: CGFloat
if isSecretMedia {
maxWidth = 180.0
maxWidth = 200.0
} else {
maxWidth = maxDimensions.width
}
if isSecretMedia {
let _ = PresentationResourcesChat.chatBubbleSecretMediaIcon(presentationData.theme.theme)
}
return (nativeSize, maxWidth, { constrainedSize, automaticPlayback, wideLayout, corners in
var resultWidth: CGFloat
@ -898,7 +897,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
switch sizeCalculation {
case .constrained:
if isSecretMedia {
boundingSize = CGSize(width: maxWidth, height: maxWidth)
boundingSize = CGSize(width: maxWidth, height: maxWidth / 5.0 * 3.0)
drawingSize = nativeSize.aspectFilled(boundingSize)
} else {
let fittedSize = nativeSize.fittedToWidthOrSmaller(boundingWidth)
@ -1149,7 +1148,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
}
if isSecretMedia {
updateImageSignal = { synchronousLoad, _ in
return chatSecretPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image))
return chatSecretPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image), ignoreFullSize: true)
}
} else {
updateImageSignal = { synchronousLoad, highQuality in
@ -1764,7 +1763,12 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
}
}
let radialStatusSize: CGFloat = wideLayout ? 50.0 : 32.0
var radialStatusSize: CGFloat
if isSecretMedia {
radialStatusSize = 48.0
} else {
radialStatusSize = wideLayout ? 50.0 : 32.0
}
if progressRequired {
if self.statusNode == nil {
let statusNode = RadialStatusNode(backgroundNodeColor: theme.chat.message.mediaOverlayControlColors.fillColor)
@ -1782,14 +1786,21 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
}
}
let messageTheme = theme.chat.message
var state: RadialStatusNodeState = .none
var backgroundColor = messageTheme.mediaOverlayControlColors.fillColor
var badgeContent: ChatMessageInteractiveMediaBadgeContent?
var mediaDownloadState: ChatMessageInteractiveMediaDownloadState?
let messageTheme = theme.chat.message
if isSecretMedia {
backgroundColor = messageTheme.mediaDateAndStatusFillColor
}
if let invoice = invoice {
if let extendedMedia = invoice.extendedMedia {
if case let .preview(_, _, maybeVideoDuration) = extendedMedia, let videoDuration = maybeVideoDuration {
badgeContent = .text(inset: 0.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: stringForDuration(videoDuration, position: nil)))
badgeContent = .text(inset: 0.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: stringForDuration(videoDuration, position: nil)), iconName: nil)
}
} else {
let string = NSMutableAttributedString()
@ -1808,7 +1819,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
}
string.append(NSAttributedString(string: title))
}
badgeContent = .text(inset: 0.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: string)
badgeContent = .text(inset: 0.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: string, iconName: nil)
}
}
var animated = animated
@ -1944,7 +1955,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
}
} else {
let progressString = String(format: "%d%%", Int(progress * 100.0))
badgeContent = .text(inset: message.flags.contains(.Unsent) ? 0.0 : 12.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: progressString))
badgeContent = .text(inset: message.flags.contains(.Unsent) ? 0.0 : 12.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: progressString), iconName: nil)
mediaDownloadState = automaticPlayback ? .none : .compactFetching(progress: 0.0)
}
@ -1972,16 +1983,10 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
}
case .Local:
state = .none
let secretProgressIcon: UIImage?
if case .constrained = sizeCalculation {
secretProgressIcon = PresentationResourcesChat.chatBubbleSecretMediaIcon(theme)
} else {
secretProgressIcon = PresentationResourcesChat.chatBubbleSecretMediaCompactIcon(theme)
}
if isSecretMedia, let (maybeBeginTime, timeout) = secretBeginTimeAndTimeout, let beginTime = maybeBeginTime {
state = .secretTimeout(color: messageTheme.mediaOverlayControlColors.foregroundColor, icon: secretProgressIcon, beginTime: beginTime, timeout: timeout, sparks: true)
} else if isSecretMedia, let secretProgressIcon = secretProgressIcon {
state = .customIcon(secretProgressIcon)
if isSecretMedia, let (maybeBeginTime, timeout) = secretBeginTimeAndTimeout, let beginTime = maybeBeginTime, Int32(timeout) != viewOnceTimeout {
state = .secretTimeout(color: messageTheme.mediaOverlayControlColors.foregroundColor, icon: .flame, beginTime: beginTime, timeout: timeout, sparks: true)
} else if isSecretMedia {
state = .staticTimeout
} else if let file = media as? TelegramMediaFile, !file.isVideoSticker {
let isInlinePlayableVideo = file.isVideo && !isSecretMedia && (self.automaticPlayback ?? false)
if (!isInlinePlayableVideo || isStory) && file.isVideo {
@ -2017,11 +2022,11 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
} else {
if isMediaStreamable(message: message, media: file) {
state = automaticPlayback ? .none : .play(messageTheme.mediaOverlayControlColors.foregroundColor)
badgeContent = .text(inset: 12.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: durationString))
badgeContent = .text(inset: 12.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: durationString), iconName: nil)
mediaDownloadState = .compactRemote
} else {
state = automaticPlayback ? .none : state
badgeContent = .text(inset: 0.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: durationString))
badgeContent = .text(inset: 0.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: durationString), iconName: nil)
}
}
}
@ -2031,16 +2036,32 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
}
}
if isSecretMedia, let (maybeBeginTime, timeout) = secretBeginTimeAndTimeout {
let remainingTime: Int32
if let beginTime = maybeBeginTime {
let elapsedTime = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - beginTime
remainingTime = Int32(max(0.0, timeout - elapsedTime))
if isSecretMedia {
let remainingTime: Int32?
if let (maybeBeginTime, timeout) = secretBeginTimeAndTimeout, Int32(timeout) != viewOnceTimeout {
if let beginTime = maybeBeginTime {
let elapsedTime = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - beginTime
remainingTime = Int32(max(0.0, timeout - elapsedTime))
} else {
remainingTime = Int32(timeout)
}
} else {
remainingTime = Int32(timeout)
if let attribute = message.autoclearAttribute {
remainingTime = attribute.timeout
} else if let attribute = message.autoremoveAttribute {
remainingTime = attribute.timeout
} else {
remainingTime = nil
}
}
badgeContent = .text(inset: 0.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: strings.MessageTimer_ShortSeconds(Int32(remainingTime))))
if let remainingTime {
if remainingTime == viewOnceTimeout {
badgeContent = .text(inset: 10.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: "1"), iconName: "Chat/Message/SecretMediaOnce")
} else {
badgeContent = .text(inset: 10.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: strings.MessageTimer_ShortSeconds(Int32(remainingTime))), iconName: "Chat/Message/SecretMediaPlay")
}
}
}
if let statusNode = self.statusNode {
@ -2062,6 +2083,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
statusNode?.removeFromSupernode()
}
})
statusNode.backgroundNodeColor = backgroundColor
}
if let badgeContent = badgeContent {
if self.badgeNode == nil {
@ -2100,11 +2122,13 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
displaySpoiler = true
} else if message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }) {
displaySpoiler = true
} else if isSecretMedia {
displaySpoiler = true
}
if displaySpoiler {
if self.extendedMediaOverlayNode == nil {
let extendedMediaOverlayNode = ExtendedMediaOverlayNode(enableAnimations: self.context?.sharedContext.energyUsageSettings.fullTranslucency ?? true)
let extendedMediaOverlayNode = ExtendedMediaOverlayNode(hasImageOverlay: !isSecretMedia, enableAnimations: self.context?.sharedContext.energyUsageSettings.fullTranslucency ?? true)
extendedMediaOverlayNode.tapped = { [weak self] in
self?.internallyVisible = true
self?.updateVisibility()
@ -2115,11 +2139,13 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
self.extendedMediaOverlayNode?.frame = self.imageNode.frame
var tappable = false
switch state {
case .play, .pause, .download, .none:
tappable = true
default:
break
if !isSecretMedia {
switch state {
case .play, .pause, .download, .none:
tappable = true
default:
break
}
}
self.extendedMediaOverlayNode?.isUserInteractionEnabled = tappable

View file

@ -281,16 +281,16 @@ final class GridMessageItemNode: GridItemNode {
switch status {
case let .Fetching(_, progress):
let progressString = String(format: "%d%%", Int(progress * 100.0))
badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: progressString))
badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: progressString), iconName: nil)
mediaDownloadState = .compactFetching(progress: 0.0)
case .Local:
badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString))
badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString), iconName: nil)
case .Remote, .Paused:
badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString))
badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString), iconName: nil)
mediaDownloadState = .compactRemote
}
} else {
badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString))
badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString), iconName: nil)
}
strongSelf.mediaBadgeNode.update(theme: item.theme, content: badgeContent, mediaDownloadState: mediaDownloadState, alignment: .right, animated: false, badgeAnimated: false)

View file

@ -675,8 +675,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
|> deliverOnMainQueue).start(next: { bot in
let choose = filterChooseTypes(choose, peerTypes: bot.peerTypes)
let botPeer = EnginePeer(bot.peer)
let controller = addWebAppToAttachmentController(context: context, peerName: botPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), icons: bot.icons, requestWriteAccess: bot.flags.contains(.requiresWriteAccess), completion: { allowWrite in
let controller = addWebAppToAttachmentController(context: context, peerName: bot.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), icons: bot.icons, requestWriteAccess: bot.flags.contains(.requiresWriteAccess), completion: { allowWrite in
let _ = (context.engine.messages.addBotToAttachMenu(botId: peerId, allowWrite: allowWrite)
|> deliverOnMainQueue).start(error: { _ in
presentError(presentationData.strings.WebApp_AddToAttachmentUnavailableError)
@ -705,7 +704,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
guard let navigationController else {
return
}
let _ = context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), attachBotStart: ChatControllerInitialAttachBotStart(botId: botPeer.id, payload: payload, justInstalled: true), useExisting: true))
let _ = context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), attachBotStart: ChatControllerInitialAttachBotStart(botId: bot.peer.id, payload: payload, justInstalled: true), useExisting: true))
}
navigationController.pushViewController(controller)
}
@ -717,7 +716,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
return
}
let _ = context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(chatPeer), attachBotStart: ChatControllerInitialAttachBotStart(botId: botPeer.id, payload: payload, justInstalled: true), useExisting: true))
let _ = context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(chatPeer), attachBotStart: ChatControllerInitialAttachBotStart(botId: bot.peer.id, payload: payload, justInstalled: true), useExisting: true))
})
}
}

View file

@ -132,6 +132,7 @@ final class TelegramGlobalSettings {
let unreadTrendingStickerPacks: Int
let archivedStickerPacks: [ArchivedStickerPackItem]?
let userLimits: EngineConfiguration.UserLimits
let bots: [AttachMenuBot]
let hasPassport: Bool
let hasWatchApp: Bool
let enableQRLogin: Bool
@ -153,6 +154,7 @@ final class TelegramGlobalSettings {
unreadTrendingStickerPacks: Int,
archivedStickerPacks: [ArchivedStickerPackItem]?,
userLimits: EngineConfiguration.UserLimits,
bots: [AttachMenuBot],
hasPassport: Bool,
hasWatchApp: Bool,
enableQRLogin: Bool
@ -173,6 +175,7 @@ final class TelegramGlobalSettings {
self.unreadTrendingStickerPacks = unreadTrendingStickerPacks
self.archivedStickerPacks = archivedStickerPacks
self.userLimits = userLimits
self.bots = bots
self.hasPassport = hasPassport
self.hasWatchApp = hasWatchApp
self.enableQRLogin = enableQRLogin
@ -508,9 +511,10 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
return automaticEnergyUsageShouldBeOn(settings: settings)
}
|> distinctUntilChanged,
hasStories
hasStories,
context.engine.messages.attachMenuBots()
)
|> map { peerView, accountsAndPeers, accountSessions, privacySettings, sharedPreferences, notifications, stickerPacks, hasPassport, hasWatchApp, accountPreferences, suggestions, limits, hasPassword, isPowerSavingEnabled, hasStories -> PeerInfoScreenData in
|> map { peerView, accountsAndPeers, accountSessions, privacySettings, sharedPreferences, notifications, stickerPacks, hasPassport, hasWatchApp, accountPreferences, suggestions, limits, hasPassword, isPowerSavingEnabled, hasStories, bots -> PeerInfoScreenData in
let (notificationExceptions, notificationsAuthorizationStatus, notificationsWarningSuppressed) = notifications
let (featuredStickerPacks, archivedStickerPacks) = stickerPacks
@ -550,6 +554,7 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
unreadTrendingStickerPacks: unreadTrendingStickerPacks,
archivedStickerPacks: archivedStickerPacks,
userLimits: peer?.isPremium == true ? limits.1 : limits.0,
bots: bots,
hasPassport: hasPassport,
hasWatchApp: hasWatchApp,
enableQRLogin: enableQRLogin)

View file

@ -91,6 +91,7 @@ import PeerInfoStoryGridScreen
import StoryContainerScreen
import ChatAvatarNavigationNode
import PeerReportScreen
import WebUI
enum PeerInfoAvatarEditingMode {
case generic
@ -549,6 +550,7 @@ private final class PeerInfoInteraction {
let toggleForumTopics: (Bool) -> Void
let displayTopicsLimited: (TopicsLimitedReason) -> Void
let openPeerMention: (String, ChatControllerInteractionNavigateToPeer) -> Void
let openBotApp: (AttachMenuBot) -> Void
init(
openUsername: @escaping (String) -> Void,
@ -599,7 +601,8 @@ private final class PeerInfoInteraction {
dismissInput: @escaping () -> Void,
toggleForumTopics: @escaping (Bool) -> Void,
displayTopicsLimited: @escaping (TopicsLimitedReason) -> Void,
openPeerMention: @escaping (String, ChatControllerInteractionNavigateToPeer) -> Void
openPeerMention: @escaping (String, ChatControllerInteractionNavigateToPeer) -> Void,
openBotApp: @escaping (AttachMenuBot) -> Void
) {
self.openUsername = openUsername
self.openPhone = openPhone
@ -650,6 +653,7 @@ private final class PeerInfoInteraction {
self.toggleForumTopics = toggleForumTopics
self.displayTopicsLimited = displayTopicsLimited
self.openPeerMention = openPeerMention
self.openBotApp = openBotApp
}
}
@ -661,7 +665,7 @@ private enum SettingsSection: Int, CaseIterable {
case phone
case accounts
case proxy
case stories
case apps
case shortcuts
case advanced
case payment
@ -792,7 +796,19 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p
}
}
items[.stories]!.append(PeerInfoScreenDisclosureItem(id: 0, text: presentationData.strings.Settings_MyStories, icon: PresentationResourcesSettings.stories, action: {
var appIndex = 1000
if let settings = data.globalSettings {
for bot in settings.bots {
if bot.flags.contains(.showInSettings) {
items[.apps]!.append(PeerInfoScreenDisclosureItem(id: appIndex, text: bot.peer.compactDisplayTitle, icon: PresentationResourcesSettings.passport, action: {
interaction.openBotApp(bot)
}))
appIndex += 1
}
}
}
items[.apps]!.append(PeerInfoScreenDisclosureItem(id: appIndex, text: presentationData.strings.Settings_MyStories, icon: PresentationResourcesSettings.stories, action: {
interaction.openSettings(.stories)
}))
@ -2340,6 +2356,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
},
openPeerMention: { [weak self] mention, navigation in
self?.openPeerMention(mention, navigation: navigation)
},
openBotApp: { [weak self] bot in
self?.openBotApp(bot)
}
)
@ -4398,7 +4417,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}, contentContext: nil)
}
private func openUrl(url: String, concealed: Bool, external: Bool) {
private func openUrl(url: String, concealed: Bool, external: Bool, forceExternal: Bool = false, commit: @escaping () -> Void = {}) {
openUserGeneratedUrl(context: self.context, peerId: self.peerId, url: url, concealed: concealed, present: { [weak self] c in
self?.controller?.present(c, in: .window(.root))
}, openResolved: { [weak self] tempResolved in
@ -4408,8 +4427,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
let result: ResolvedUrl = external ? .externalUrl(url) : tempResolved
strongSelf.context.sharedContext.openResolvedUrl(result, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.controller?.navigationController as? NavigationController, forceExternal: false, openPeer: { peer, navigation in
strongSelf.context.sharedContext.openResolvedUrl(result, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.controller?.navigationController as? NavigationController, forceExternal: forceExternal, openPeer: { peer, navigation in
self?.openPeer(peerId: peer.id, navigation: navigation)
commit()
}, sendFile: nil,
sendSticker: nil,
requestMessageActionUrlAuth: nil,
@ -4579,6 +4599,51 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}))
}
private let openBotAppDisposable = MetaDisposable()
private func openBotApp(_ bot: AttachMenuBot) {
guard let controller = self.controller else {
return
}
let proceed = { [weak self] in
guard let self else {
return
}
self.openBotAppDisposable.set(((self.context.engine.messages.requestSimpleWebView(botId: bot.peer.id, url: nil, source: .settings, themeParams: generateWebAppThemeParams(self.presentationData.theme))
|> afterDisposed {
// updateProgress()
})
|> deliverOnMainQueue).start(next: { [weak self] url in
guard let self else {
return
}
let params = WebAppParameters(peerId: self.context.account.peerId, botId: bot.peer.id, botName: bot.peer.compactDisplayTitle, url: url, queryId: nil, payload: nil, buttonText: nil, keepAliveSignal: nil, fromMenu: false, fromAttachMenu: false, isInline: false, isSimple: true)
let controller = standaloneWebAppController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, params: params, threadId: nil, openUrl: { [weak self] url, concealed, commit in
self?.openUrl(url: url, concealed: concealed, external: false, forceExternal: true, commit: commit)
}, requestSwitchInline: { _, _, _ in
}, getNavigationController: { [weak self] in
return self?.controller?.navigationController as? NavigationController
})
controller.navigationPresentation = .flatModal
self.controller?.push(controller)
}, error: { [weak self] error in
if let self {
self.controller?.present(textAlertController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, title: nil, text: self.presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {
})]), in: .window(.root))
}
}))
}
if bot.flags.contains(.showInSettingsDisclaimer) {
let alertController = webAppTermsAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, peer: bot.peer, completion: {
proceed()
})
controller.present(alertController, in: .window(.root))
} else {
proceed()
}
}
private func performButtonAction(key: PeerInfoHeaderButtonKey, gesture: ContextGesture?) {
guard let controller = self.controller else {
return

View file

@ -385,16 +385,16 @@ private final class VisualMediaItemNode: ASDisplayNode {
switch status {
case let .Fetching(_, progress):
let progressString = String(format: "%d%%", Int(progress * 100.0))
badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: progressString))
badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: progressString), iconName: nil)
mediaDownloadState = .compactFetching(progress: 0.0)
case .Local:
badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString))
badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString), iconName: nil)
case .Remote, .Paused:
badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString))
badgeContent = .text(inset: 12.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString), iconName: nil)
mediaDownloadState = .compactRemote
}
} else {
badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString))
badgeContent = .text(inset: 0.0, backgroundColor: mediaBadgeBackgroundColor, foregroundColor: mediaBadgeTextColor, text: NSAttributedString(string: durationString), iconName: nil)
}
strongSelf.mediaBadgeNode.update(theme: nil, content: badgeContent, mediaDownloadState: mediaDownloadState, alignment: .right, animated: false, badgeAnimated: false)

View file

@ -39,6 +39,7 @@ import AttachmentTextInputPanelNode
import ChatEntityKeyboardInputNode
import HashtagSearchUI
import PeerInfoStoryGridScreen
import LegacyMessageInputPanel
private final class AccountUserInterfaceInUseContext {
let subscribers = Bag<(Bool) -> Void>()
@ -1624,98 +1625,107 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}
public func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, customEmojiAvailable: Bool, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject? {
var presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
// var presentationData = context.sharedContext.currentPresentationData.with { $0 }
// presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
//
// var presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, limitsConfiguration: context.currentLimitsConfiguration.with { $0 }, fontSize: presentationData.chatFontSize, bubbleCorners: presentationData.chatBubbleCorners, accountPeerId: context.account.peerId, mode: .standard(previewing: false), chatLocation: chatLocation, subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil)
//
// var updateChatPresentationInterfaceStateImpl: (((ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) -> Void)?
// var ensureFocusedImpl: (() -> Void)?
//
// let interfaceInteraction = ChatPanelInterfaceInteraction(updateTextInputStateAndMode: { f in
// updateChatPresentationInterfaceStateImpl?({
// let (updatedState, updatedMode) = f($0.interfaceState.effectiveInputState, $0.inputMode)
// return $0.updatedInterfaceState { interfaceState in
// return interfaceState.withUpdatedEffectiveInputState(updatedState)
// }.updatedInputMode({ _ in updatedMode })
// })
// }, updateInputModeAndDismissedButtonKeyboardMessageId: { f in
// updateChatPresentationInterfaceStateImpl?({
// let (updatedInputMode, updatedClosedButtonKeyboardMessageId) = f($0)
// return $0.updatedInputMode({ _ in return updatedInputMode }).updatedInterfaceState({
// $0.withUpdatedMessageActionsState({ value in
// var value = value
// value.closedButtonKeyboardMessageId = updatedClosedButtonKeyboardMessageId
// return value
// })
// })
// })
// }, openLinkEditing: {
// var selectionRange: Range<Int>?
// var text: NSAttributedString?
// var inputMode: ChatInputMode?
// updateChatPresentationInterfaceStateImpl?({ state in
// selectionRange = state.interfaceState.effectiveInputState.selectionRange
// if let selectionRange = selectionRange {
// text = state.interfaceState.effectiveInputState.inputText.attributedSubstring(from: NSRange(location: selectionRange.startIndex, length: selectionRange.count))
// }
// inputMode = state.inputMode
// return state
// })
//
// var link: String?
// if let text {
// text.enumerateAttributes(in: NSMakeRange(0, text.length)) { attributes, _, _ in
// if let linkAttribute = attributes[ChatTextInputAttributes.textUrl] as? ChatTextInputTextUrlAttribute {
// link = linkAttribute.url
// }
// }
// }
//
// let controller = chatTextLinkEditController(sharedContext: context.sharedContext, updatedPresentationData: (presentationData, .never()), account: context.account, text: text?.string ?? "", link: link, apply: { link in
// if let inputMode = inputMode, let selectionRange = selectionRange {
// if let link = link {
// updateChatPresentationInterfaceStateImpl?({
// return $0.updatedInterfaceState({
// $0.withUpdatedEffectiveInputState(chatTextInputAddLinkAttribute($0.effectiveInputState, selectionRange: selectionRange, url: link))
// })
// })
// }
// ensureFocusedImpl?()
// updateChatPresentationInterfaceStateImpl?({
// return $0.updatedInputMode({ _ in return inputMode }).updatedInterfaceState({
// $0.withUpdatedEffectiveInputState(ChatTextInputState(inputText: $0.effectiveInputState.inputText, selectionRange: selectionRange.endIndex ..< selectionRange.endIndex))
// })
// })
// }
// })
// present(controller)
// })
//
// let inputPanelNode = AttachmentTextInputPanelNode(context: context, presentationInterfaceState: presentationInterfaceState, isCaption: true, presentController: { c in
// presentInGlobalOverlay(c)
// }, makeEntityInputView: {
// return EntityInputView(context: context, isDark: true, areCustomEmojiEnabled: customEmojiAvailable)
// })
// inputPanelNode.interfaceInteraction = interfaceInteraction
// inputPanelNode.effectivePresentationInterfaceState = {
// return presentationInterfaceState
// }
//
// updateChatPresentationInterfaceStateImpl = { [weak inputPanelNode] f in
// let updatedPresentationInterfaceState = f(presentationInterfaceState)
// let updateInputTextState = presentationInterfaceState.interfaceState.effectiveInputState != updatedPresentationInterfaceState.interfaceState.effectiveInputState
//
// presentationInterfaceState = updatedPresentationInterfaceState
//
// if let inputPanelNode = inputPanelNode, updateInputTextState {
// inputPanelNode.updateInputTextState(updatedPresentationInterfaceState.interfaceState.effectiveInputState, animated: true)
// }
// }
//
// ensureFocusedImpl = { [weak inputPanelNode] in
// inputPanelNode?.ensureFocused()
// }
//
// return inputPanelNode
var presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, limitsConfiguration: context.currentLimitsConfiguration.with { $0 }, fontSize: presentationData.chatFontSize, bubbleCorners: presentationData.chatBubbleCorners, accountPeerId: context.account.peerId, mode: .standard(previewing: false), chatLocation: chatLocation, subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil)
var updateChatPresentationInterfaceStateImpl: (((ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) -> Void)?
var ensureFocusedImpl: (() -> Void)?
let interfaceInteraction = ChatPanelInterfaceInteraction(updateTextInputStateAndMode: { f in
updateChatPresentationInterfaceStateImpl?({
let (updatedState, updatedMode) = f($0.interfaceState.effectiveInputState, $0.inputMode)
return $0.updatedInterfaceState { interfaceState in
return interfaceState.withUpdatedEffectiveInputState(updatedState)
}.updatedInputMode({ _ in updatedMode })
})
}, updateInputModeAndDismissedButtonKeyboardMessageId: { f in
updateChatPresentationInterfaceStateImpl?({
let (updatedInputMode, updatedClosedButtonKeyboardMessageId) = f($0)
return $0.updatedInputMode({ _ in return updatedInputMode }).updatedInterfaceState({
$0.withUpdatedMessageActionsState({ value in
var value = value
value.closedButtonKeyboardMessageId = updatedClosedButtonKeyboardMessageId
return value
})
})
})
}, openLinkEditing: {
var selectionRange: Range<Int>?
var text: NSAttributedString?
var inputMode: ChatInputMode?
updateChatPresentationInterfaceStateImpl?({ state in
selectionRange = state.interfaceState.effectiveInputState.selectionRange
if let selectionRange = selectionRange {
text = state.interfaceState.effectiveInputState.inputText.attributedSubstring(from: NSRange(location: selectionRange.startIndex, length: selectionRange.count))
}
inputMode = state.inputMode
return state
})
var link: String?
if let text {
text.enumerateAttributes(in: NSMakeRange(0, text.length)) { attributes, _, _ in
if let linkAttribute = attributes[ChatTextInputAttributes.textUrl] as? ChatTextInputTextUrlAttribute {
link = linkAttribute.url
}
}
}
let controller = chatTextLinkEditController(sharedContext: context.sharedContext, updatedPresentationData: (presentationData, .never()), account: context.account, text: text?.string ?? "", link: link, apply: { link in
if let inputMode = inputMode, let selectionRange = selectionRange {
if let link = link {
updateChatPresentationInterfaceStateImpl?({
return $0.updatedInterfaceState({
$0.withUpdatedEffectiveInputState(chatTextInputAddLinkAttribute($0.effectiveInputState, selectionRange: selectionRange, url: link))
})
})
}
ensureFocusedImpl?()
updateChatPresentationInterfaceStateImpl?({
return $0.updatedInputMode({ _ in return inputMode }).updatedInterfaceState({
$0.withUpdatedEffectiveInputState(ChatTextInputState(inputText: $0.effectiveInputState.inputText, selectionRange: selectionRange.endIndex ..< selectionRange.endIndex))
})
})
}
})
present(controller)
})
let inputPanelNode = AttachmentTextInputPanelNode(context: context, presentationInterfaceState: presentationInterfaceState, isCaption: true, presentController: { c in
presentInGlobalOverlay(c)
}, makeEntityInputView: {
return EntityInputView(context: context, isDark: true, areCustomEmojiEnabled: customEmojiAvailable)
})
inputPanelNode.interfaceInteraction = interfaceInteraction
inputPanelNode.effectivePresentationInterfaceState = {
return presentationInterfaceState
}
updateChatPresentationInterfaceStateImpl = { [weak inputPanelNode] f in
let updatedPresentationInterfaceState = f(presentationInterfaceState)
let updateInputTextState = presentationInterfaceState.interfaceState.effectiveInputState != updatedPresentationInterfaceState.interfaceState.effectiveInputState
presentationInterfaceState = updatedPresentationInterfaceState
if let inputPanelNode = inputPanelNode, updateInputTextState {
inputPanelNode.updateInputTextState(updatedPresentationInterfaceState.interfaceState.effectiveInputState, animated: true)
}
}
ensureFocusedImpl = { [weak inputPanelNode] in
inputPanelNode?.ensureFocused()
}
let inputPanelNode = LegacyMessageInputPanelNode(
context: context,
chatLocation: chatLocation,
present: present,
presentInGlobalOverlay: presentInGlobalOverlay
)
return inputPanelNode
}

View file

@ -111,6 +111,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
private let text: TooltipScreen.Text
private let textAlignment: TooltipScreen.Alignment
private let balancedTextLayout: Bool
private let constrainWidth: CGFloat?
private let tooltipStyle: TooltipScreen.Style
private let arrowStyle: TooltipScreen.ArrowStyle
private let icon: TooltipScreen.Icon?
@ -159,6 +160,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
text: TooltipScreen.Text,
textAlignment: TooltipScreen.Alignment,
balancedTextLayout: Bool,
constrainWidth: CGFloat?,
style: TooltipScreen.Style,
arrowStyle: TooltipScreen.ArrowStyle,
icon: TooltipScreen.Icon? = nil,
@ -379,6 +381,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
self.text = text
self.textAlignment = textAlignment
self.balancedTextLayout = balancedTextLayout
self.constrainWidth = constrainWidth
self.animatedStickerNode = DefaultAnimatedStickerNodeImpl()
switch icon {
@ -477,7 +480,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
} else {
animationSize = CGSize(width: 32.0, height: 32.0)
}
if animationName == "anim_autoremove_on" {
if ["anim_autoremove_on", "anim_autoremove_off"].contains(animationName) {
animationOffset = -3.0
} else if animationName == "ChatListFoldersTooltip" {
animationInset = (70.0 - animationSize.width) / 2.0
@ -491,7 +494,10 @@ private final class TooltipScreenNode: ViewControllerTracingNode {
animationSpacing = 8.0
}
let containerWidth = max(100.0, min(layout.size.width - sideInset * 2.0, 614.0))
var containerWidth = max(100.0, min(layout.size.width - sideInset * 2.0, 614.0))
if let constrainWidth = self.constrainWidth, constrainWidth > 100.0 {
containerWidth = constrainWidth
}
var actionSize: CGSize = .zero
@ -1001,6 +1007,7 @@ public final class TooltipScreen: ViewController {
public let text: TooltipScreen.Text
public let textAlignment: TooltipScreen.Alignment
private let balancedTextLayout: Bool
private let constrainWidth: CGFloat?
private let style: TooltipScreen.Style
private let arrowStyle: TooltipScreen.ArrowStyle
private let icon: TooltipScreen.Icon?
@ -1039,6 +1046,7 @@ public final class TooltipScreen: ViewController {
text: TooltipScreen.Text,
textAlignment: TooltipScreen.Alignment = .natural,
balancedTextLayout: Bool = false,
constrainWidth: CGFloat? = nil,
style: TooltipScreen.Style = .default,
arrowStyle: TooltipScreen.ArrowStyle = .default,
icon: TooltipScreen.Icon? = nil,
@ -1056,6 +1064,7 @@ public final class TooltipScreen: ViewController {
self.text = text
self.textAlignment = textAlignment
self.balancedTextLayout = balancedTextLayout
self.constrainWidth = constrainWidth
self.style = style
self.arrowStyle = arrowStyle
self.icon = icon
@ -1123,7 +1132,7 @@ public final class TooltipScreen: ViewController {
}
override public func loadDisplayNode() {
self.displayNode = TooltipScreenNode(context: self.context, account: self.account, sharedContext: self.sharedContext, text: self.text, textAlignment: self.textAlignment, balancedTextLayout: self.balancedTextLayout, style: self.style, arrowStyle: self.arrowStyle, icon: self.icon, action: self.action, location: self.location, displayDuration: self.displayDuration, inset: self.inset, cornerRadius: self.cornerRadius, shouldDismissOnTouch: self.shouldDismissOnTouch, requestDismiss: { [weak self] in
self.displayNode = TooltipScreenNode(context: self.context, account: self.account, sharedContext: self.sharedContext, text: self.text, textAlignment: self.textAlignment, balancedTextLayout: self.balancedTextLayout, constrainWidth: self.constrainWidth, style: self.style, arrowStyle: self.arrowStyle, icon: self.icon, action: self.action, location: self.location, displayDuration: self.displayDuration, inset: self.inset, cornerRadius: self.cornerRadius, shouldDismissOnTouch: self.shouldDismissOnTouch, requestDismiss: { [weak self] in
guard let strongSelf = self else {
return
}

View file

@ -962,7 +962,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
let link = MarkdownAttributeSet(font: Font.regular(14.0), textColor: undoTextColor)
let link = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: undoTextColor)
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { contents in
return ("URL", contents)
}), textAlignment: .natural)
@ -1347,8 +1347,12 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
var panelWrapperFrame = CGRect(origin: CGPoint(x: leftMargin + layout.safeInsets.left, y: layout.size.height - contentHeight - insets.bottom - margin), size: CGSize(width: layout.size.width - leftMargin * 2.0 - layout.safeInsets.left - layout.safeInsets.right, height: contentHeight))
if case .top = self.placementPosition {
panelFrame.origin.y = insets.top + margin
panelWrapperFrame.origin.y = insets.top + margin
var topInset = insets.top
if topInset.isZero {
topInset = layout.statusBarHeight ?? 44.0
}
panelFrame.origin.y = topInset + margin
panelWrapperFrame.origin.y = topInset + margin
}
transition.updateFrame(node: self.panelNode, frame: panelFrame)
@ -1441,7 +1445,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
let statusSize: CGFloat = 30.0
transition.updateFrame(node: statusNode, frame: CGRect(origin: CGPoint(x: floor((leftInset - statusSize) / 2.0), y: floor((contentHeight - statusSize) / 2.0)), size: CGSize(width: statusSize, height: statusSize)))
if firstLayout {
statusNode.transitionToState(.secretTimeout(color: .white, icon: nil, beginTime: CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, timeout: Double(self.remainingSeconds), sparks: false), completion: {})
statusNode.transitionToState(.secretTimeout(color: .white, icon: .none, beginTime: CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, timeout: Double(self.remainingSeconds), sparks: false), completion: {})
}
}

View file

@ -38,6 +38,8 @@ public class WebAppCancelButtonNode: ASDisplayNode {
public var state: State = .cancel
private var color: UIColor?
private var _theme: PresentationTheme
public var theme: PresentationTheme {
get {
@ -50,6 +52,24 @@ public class WebAppCancelButtonNode: ASDisplayNode {
}
private let strings: PresentationStrings
public func updateColor(_ color: UIColor?, transition: ContainedViewLayoutTransition) {
self.color = color
if case let .animated(duration, curve) = transition {
if let snapshotView = self.view.snapshotContentTree() {
snapshotView.frame = self.bounds
self.view.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false, completion: { _ in
snapshotView.removeFromSuperview()
})
self.arrowNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction)
self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction)
}
}
self.setState(self.state, animated: false, animateScale: false, force: true)
}
public init(theme: PresentationTheme, strings: PresentationStrings) {
self._theme = theme
self.strings = strings
@ -124,13 +144,15 @@ public class WebAppCancelButtonNode: ASDisplayNode {
self.buttonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
}
let color = self.color ?? self.theme.rootController.navigationBar.accentTextColor
self.arrowNode.isHidden = state == .cancel
self.labelNode.attributedText = NSAttributedString(string: state == .cancel ? self.strings.Common_Cancel : self.strings.Common_Back, font: Font.regular(17.0), textColor: self.theme.rootController.navigationBar.accentTextColor)
self.labelNode.attributedText = NSAttributedString(string: state == .cancel ? self.strings.Common_Cancel : self.strings.Common_Back, font: Font.regular(17.0), textColor: color)
let labelSize = self.labelNode.updateLayout(CGSize(width: 120.0, height: 56.0))
self.buttonNode.frame = CGRect(origin: .zero, size: CGSize(width: labelSize.width, height: self.buttonNode.frame.height))
self.arrowNode.image = NavigationBarTheme.generateBackArrowImage(color: self.theme.rootController.navigationBar.accentTextColor)
self.arrowNode.image = NavigationBarTheme.generateBackArrowImage(color: color)
if let image = self.arrowNode.image {
self.arrowNode.frame = CGRect(origin: self.arrowNode.frame.origin, size: image.size)
}
@ -305,7 +327,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
return .complete()
}
|> mapToSignal { bot -> Signal<(FileMediaReference, Bool)?, NoError> in
if let bot = bot, let peerReference = PeerReference(bot.peer) {
if let bot = bot, let peerReference = PeerReference(bot.peer._asPeer()) {
var imageFile: TelegramMediaFile?
var isPlaceholder = false
if let file = bot.icons[.placeholder] {
@ -546,12 +568,16 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
self.controller?.present(promptController, in: .window(.root))
}
private func updateNavigationBarAlpha(transition: ContainedViewLayoutTransition) {
let contentOffset = self.webView?.scrollView.contentOffset.y ?? 0.0
let backgroundAlpha = min(30.0, contentOffset) / 30.0
self.controller?.navigationBar?.updateBackgroundAlpha(backgroundAlpha, transition: transition)
}
private var targetContentOffset: CGPoint?
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let contentOffset = scrollView.contentOffset.y
self.controller?.navigationBar?.updateBackgroundAlpha(min(30.0, contentOffset) / 30.0, transition: .immediate)
self.updateNavigationBarAlpha(transition: .immediate)
if let targetContentOffset = self.targetContentOffset, scrollView.contentOffset != targetContentOffset {
scrollView.contentOffset = targetContentOffset
}
@ -821,8 +847,14 @@ public final class WebAppController: ViewController, AttachmentContainable {
transition.updateBackgroundColor(node: self.backgroundNode, color: color)
}
case "web_app_set_header_color":
if let json = json, let colorKey = json["color_key"] as? String, ["bg_color", "secondary_bg_color"].contains(colorKey) {
self.headerColorKey = colorKey
if let json = json {
if let colorKey = json["color_key"] as? String, ["bg_color", "secondary_bg_color"].contains(colorKey) {
self.headerColor = nil
self.headerColorKey = colorKey
} else if let hexColor = json["color"] as? String, let color = UIColor(hexString: hexColor) {
self.headerColor = color
self.headerColorKey = nil
}
self.updateHeaderBackgroundColor(transition: .animated(duration: 0.2, curve: .linear))
}
case "web_app_open_popup":
@ -938,16 +970,37 @@ public final class WebAppController: ViewController, AttachmentContainable {
fileprivate var needDismissConfirmation = false
fileprivate var headerColor: UIColor?
fileprivate var headerPrimaryTextColor: UIColor?
private var headerColorKey: String?
private func updateHeaderBackgroundColor(transition: ContainedViewLayoutTransition) {
guard let controller = self.controller else {
return
}
let color: UIColor?
var primaryTextColor: UIColor?
var secondaryTextColor: UIColor?
var backgroundColor = self.presentationData.theme.list.plainBackgroundColor
var secondaryBackgroundColor = self.presentationData.theme.list.blocksBackgroundColor
if self.presentationData.theme.list.blocksBackgroundColor.rgb == self.presentationData.theme.list.plainBackgroundColor.rgb {
backgroundColor = self.presentationData.theme.list.modalPlainBackgroundColor
secondaryBackgroundColor = self.presentationData.theme.list.plainBackgroundColor
}
if let headerColorKey = self.headerColorKey {
if let headerColor = self.headerColor {
color = headerColor
let textColor = headerColor.lightness > 0.5 ? UIColor(rgb: 0x000000) : UIColor(rgb: 0xffffff)
func calculateSecondaryAlpha(luminance: CGFloat, targetContrast: CGFloat) -> CGFloat {
let targetLuminance = luminance > 0.5 ? 0.0 : 1.0
let adaptiveAlpha = (luminance - targetLuminance + targetContrast) / targetContrast
return max(0.5, min(0.64, adaptiveAlpha))
}
primaryTextColor = textColor
self.headerPrimaryTextColor = textColor
secondaryTextColor = textColor.withAlphaComponent(calculateSecondaryAlpha(luminance: headerColor.lightness, targetContrast: 2.5))
} else if let headerColorKey = self.headerColorKey {
switch headerColorKey {
case "bg_color":
color = backgroundColor
@ -959,6 +1012,13 @@ public final class WebAppController: ViewController, AttachmentContainable {
} else {
color = nil
}
self.updateNavigationBarAlpha(transition: transition)
controller.updateNavigationBarTheme(transition: transition)
controller.titleView?.updateTextColors(primary: primaryTextColor, secondary: secondaryTextColor, transition: transition)
controller.cancelButtonNode.updateColor(primaryTextColor, transition: transition)
controller.moreButtonNode.updateColor(primaryTextColor, transition: transition)
transition.updateBackgroundColor(node: self.headerBackgroundNode, color: color ?? .clear)
transition.updateBackgroundColor(node: self.topOverscrollNode, color: color ?? .clear)
}
@ -1234,8 +1294,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
private var titleView: CounterContollerTitleView?
private let cancelButtonNode: WebAppCancelButtonNode
private let moreButtonNode: MoreButtonNode
fileprivate let cancelButtonNode: WebAppCancelButtonNode
fileprivate let moreButtonNode: MoreButtonNode
private let context: AccountContext
private let peerId: PeerId
@ -1318,10 +1378,9 @@ public final class WebAppController: ViewController, AttachmentContainable {
if let strongSelf = self {
strongSelf.presentationData = presentationData
let navigationBarPresentationData = NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: presentationData.theme), strings: NavigationBarStrings(back: "", close: ""))
strongSelf.navigationBar?.updatePresentationData(navigationBarPresentationData)
strongSelf.updateNavigationBarTheme(transition: .immediate)
strongSelf.titleView?.theme = presentationData.theme
strongSelf.cancelButtonNode.theme = presentationData.theme
strongSelf.moreButtonNode.theme = presentationData.theme
@ -1341,6 +1400,32 @@ public final class WebAppController: ViewController, AttachmentContainable {
self.presentationDataDisposable?.dispose()
}
fileprivate func updateNavigationBarTheme(transition: ContainedViewLayoutTransition) {
let navigationBarPresentationData: NavigationBarPresentationData
if let backgroundColor = self.controllerNode.headerColor, let textColor = self.controllerNode.headerPrimaryTextColor {
navigationBarPresentationData = NavigationBarPresentationData(
theme: NavigationBarTheme(
buttonColor: textColor,
disabledButtonColor: textColor,
primaryTextColor: textColor,
backgroundColor: backgroundColor,
enableBackgroundBlur: true,
separatorColor: UIColor(rgb: 0x000000, alpha: 0.25),
badgeBackgroundColor: .clear,
badgeStrokeColor: .clear,
badgeTextColor: .clear
),
strings: NavigationBarStrings(back: "", close: "")
)
} else {
navigationBarPresentationData = NavigationBarPresentationData(
theme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme),
strings: NavigationBarStrings(back: "", close: "")
)
}
self.navigationBar?.updatePresentationData(navigationBarPresentationData)
}
@objc private func cancelPressed() {
if case .back = self.cancelButtonNode.state {
self.controllerNode.sendBackButtonEvent()

View file

@ -0,0 +1,302 @@
import Foundation
import UIKit
import SwiftSignalKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import AppBundle
import AvatarNode
import CheckNode
import Markdown
private let textFont = Font.regular(13.0)
private let boldTextFont = Font.semibold(13.0)
private func formattedText(_ text: String, color: UIColor, linkColor: UIColor, textAlignment: NSTextAlignment = .natural) -> NSAttributedString {
return parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: color), bold: MarkdownAttributeSet(font: boldTextFont, textColor: color), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { _ in return nil}), textAlignment: textAlignment)
}
private final class WebAppTermsAlertContentNode: AlertContentNode {
private let strings: PresentationStrings
private let title: String
private let text: String
private let titleNode: ImmediateTextNode
private let textNode: ASTextNode
private let acceptTermsCheckNode: InteractiveCheckNode
private let acceptTermsLabelNode: ASTextNode
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private var validLayout: CGSize?
override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
var acceptedTerms: Bool = false {
didSet {
self.acceptTermsCheckNode.setSelected(self.acceptedTerms, animated: true)
if let firstAction = self.actionNodes.first {
firstAction.actionEnabled = self.acceptedTerms
}
}
}
init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, title: String, text: String, actions: [TextAlertAction]) {
self.strings = strings
self.title = title
self.text = text
self.titleNode = ImmediateTextNode()
self.titleNode.displaysAsynchronously = false
self.titleNode.maximumNumberOfLines = 1
self.titleNode.textAlignment = .center
self.textNode = ASTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.maximumNumberOfLines = 0
self.acceptTermsCheckNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: theme.accentColor, strokeColor: theme.contrastColor, borderColor: theme.controlBorderColor, overlayBorder: false, hasInset: false, hasShadow: false))
self.acceptTermsLabelNode = ASTextNode()
self.acceptTermsLabelNode.maximumNumberOfLines = 4
self.acceptTermsLabelNode.isUserInteractionEnabled = true
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.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.acceptTermsCheckNode)
self.addSubnode(self.acceptTermsLabelNode)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
if let firstAction = self.actionNodes.first {
firstAction.actionEnabled = false
}
self.acceptTermsCheckNode.valueChanged = { [weak self] value in
if let strongSelf = self {
strongSelf.acceptedTerms = !strongSelf.acceptedTerms
}
}
self.updateTheme(theme)
}
override func didLoad() {
super.didLoad()
self.acceptTermsLabelNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.acceptTap(_:))))
}
@objc private func acceptTap(_ gestureRecognizer: UITapGestureRecognizer) {
if self.acceptTermsCheckNode.isUserInteractionEnabled {
self.acceptedTerms = !self.acceptedTerms
}
}
override func updateTheme(_ theme: AlertControllerTheme) {
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor, paragraphAlignment: .center)
self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center)
let text = "I agree to the [Terms of Use]()"
self.acceptTermsLabelNode.attributedText = formattedText(text, color: theme.primaryColor, linkColor: theme.accentColor)
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)
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 17.0)
let titleSize = self.titleNode.updateLayout(CGSize(width: size.width - 32.0, height: size.height))
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
origin.y += titleSize.height + 4.0
var entriesHeight: CGFloat = 0.0
let textSize = self.textNode.measure(CGSize(width: size.width - 48.0, height: size.height))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
origin.y += textSize.height
if self.acceptTermsLabelNode.supernode != nil {
origin.y += 21.0
entriesHeight += 21.0
let checkSize = CGSize(width: 22.0, height: 22.0)
let condensedSize = CGSize(width: size.width - 76.0, height: size.height)
let spacing: CGFloat = 12.0
let acceptTermsSize = self.acceptTermsLabelNode.measure(condensedSize)
let acceptTermsTotalWidth = checkSize.width + spacing + acceptTermsSize.width
let acceptTermsOriginX = floorToScreenPixels((size.width - acceptTermsTotalWidth) / 2.0)
transition.updateFrame(node: self.acceptTermsCheckNode, frame: CGRect(origin: CGPoint(x: acceptTermsOriginX, y: origin.y - 3.0), size: checkSize))
transition.updateFrame(node: self.acceptTermsLabelNode, frame: CGRect(origin: CGPoint(x: acceptTermsOriginX + checkSize.width + spacing, y: origin.y), size: acceptTermsSize))
origin.y += acceptTermsSize.height
entriesHeight += acceptTermsSize.height
}
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.vertical
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: 18.0, right: 18.0)
let contentWidth = max(size.width, minActionsWidth)
var actionsHeight: CGFloat = 0.0
switch effectiveActionLayout {
case .horizontal:
actionsHeight = actionButtonHeight
case .vertical:
actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
}
let resultSize = CGSize(width: contentWidth, height: titleSize.height + textSize.height + entriesHeight + actionsHeight + 3.0 + 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
}
return resultSize
}
}
public func webAppTermsAlertController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peer: EnginePeer, completion: @escaping () -> Void) -> AlertController {
let theme = defaultDarkColorPresentationTheme
let presentationData: PresentationData
if let updatedPresentationData {
presentationData = updatedPresentationData.initial
} else {
presentationData = context.sharedContext.currentPresentationData.with { $0 }
}
let strings = presentationData.strings
var dismissImpl: ((Bool) -> Void)?
let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: "Continue", action: {
completion()
dismissImpl?(true)
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?(true)
})]
let title = "Warning"
let text = "You are about to use a mini app operated by an independent party not affiliated with Telegram. You must agree to the Terms of Use of mini apps to continue."
let contentNode = WebAppTermsAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, title: title, text: text, actions: actions)
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode)
dismissImpl = { [weak controller] animated in
if animated {
controller?.dismissAnimated()
} else {
controller?.dismiss()
}
}
return controller
}

Some files were not shown because too many files have changed in this diff Show more