Cherry-pick media timer and web app improvements
This commit is contained in:
parent
2cc863bb04
commit
6e76fa8bec
102 changed files with 3940 additions and 644 deletions
|
|
@ -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.";
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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?()
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ extension UIView {
|
|||
}
|
||||
|
||||
open class ComponentState {
|
||||
var _updated: ((Transition) -> Void)?
|
||||
open var _updated: ((Transition) -> Void)?
|
||||
var isUpdated: Bool = false
|
||||
|
||||
public init() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ swift_library(
|
|||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/LegacyComponents:LegacyComponents",
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/ManagedAnimationNode:ManagedAnimationNode"
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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: {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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! {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,10 +133,7 @@ public enum PresentationResourceKey: Int32 {
|
|||
case chatMediaConsumableContentIcon
|
||||
|
||||
case chatBubbleMediaOverlayControlSecret
|
||||
|
||||
case chatBubbleSecretMediaIcon
|
||||
case chatBubbleSecretMediaCompactIcon
|
||||
|
||||
|
||||
case chatInstantVideoWithWallpaperBackgroundImage
|
||||
case chatInstantVideoWithoutWallpaperBackgroundImage
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
]
|
||||
))
|
||||
)),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
])
|
||||
)))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 |
12
submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaOnce.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaOnce.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "miniplayonce.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
107
submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaOnce.imageset/miniplayonce.pdf
vendored
Normal file
107
submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaOnce.imageset/miniplayonce.pdf
vendored
Normal 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
|
||||
12
submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaPlay.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaPlay.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "miniplay.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
73
submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaPlay.imageset/miniplay.pdf
vendored
Normal file
73
submodules/TelegramUI/Images.xcassets/Chat/Message/SecretMediaPlay.imageset/miniplay.pdf
vendored
Normal 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
|
||||
12
submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnce.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnce.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "viewonce_30.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
212
submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnce.imageset/viewonce_30.pdf
vendored
Normal file
212
submodules/TelegramUI/Images.xcassets/Media Gallery/ViewOnce.imageset/viewonce_30.pdf
vendored
Normal 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
|
||||
163
submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/AvatarBoost.pdf
vendored
Normal file
163
submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/AvatarBoost.pdf
vendored
Normal 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
|
||||
12
submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Premium/AvatarBoost.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AvatarBoost.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
79
submodules/TelegramUI/Images.xcassets/Premium/Boost.imageset/Boost.pdf
vendored
Normal file
79
submodules/TelegramUI/Images.xcassets/Premium/Boost.imageset/Boost.pdf
vendored
Normal 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
|
||||
12
submodules/TelegramUI/Images.xcassets/Premium/Boost.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Premium/Boost.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Boost.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
12
submodules/TelegramUI/Images.xcassets/Premium/BoostChannel.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Premium/BoostChannel.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "SmallBoost.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
79
submodules/TelegramUI/Images.xcassets/Premium/BoostChannel.imageset/SmallBoost.pdf
vendored
Normal file
79
submodules/TelegramUI/Images.xcassets/Premium/BoostChannel.imageset/SmallBoost.pdf
vendored
Normal 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
|
||||
12
submodules/TelegramUI/Images.xcassets/Premium/CopyLink.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Premium/CopyLink.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Copy.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
402
submodules/TelegramUI/Images.xcassets/Premium/CopyLink.imageset/Copy.pdf
vendored
Normal file
402
submodules/TelegramUI/Images.xcassets/Premium/CopyLink.imageset/Copy.pdf
vendored
Normal 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
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
302
submodules/WebUI/Sources/WebAppTermsAlertController.swift
Normal file
302
submodules/WebUI/Sources/WebAppTermsAlertController.swift
Normal 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
Loading…
Add table
Add a link
Reference in a new issue