Merge commit '650ec2296eb46a54ab01fa0241e75a7a5f3a5ddc'

This commit is contained in:
Ali 2023-09-01 16:12:52 +04:00
commit 0192d976c8
27 changed files with 675 additions and 186 deletions

View File

@ -9863,6 +9863,8 @@ Sorry for the inconvenience.";
"Story.ViewList.ViewerCount_any" = "%d Viewers"; "Story.ViewList.ViewerCount_any" = "%d Viewers";
"AuthSessions.MessageApp" = "You allowed this bot to message you when you opened %@."; "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.";
"Story.Privacy.PostStoryAs" = "Post Story As"; "Story.Privacy.PostStoryAs" = "Post Story As";
"Story.Privacy.PostStoryAsHeader" = "POST STORY AS"; "Story.Privacy.PostStoryAsHeader" = "POST STORY AS";
@ -9870,3 +9872,32 @@ Sorry for the inconvenience.";
"Story.Privacy.KeepOnChannelPageInfo" = "Keep this story on channel profile even after it expires in %@."; "Story.Privacy.KeepOnChannelPageInfo" = "Keep this story on channel profile even after it expires in %@.";
"Story.Editor.TooltipPremiumReaction" = "Subscribe to [Telegram Premium]() to use this reaction."; "Story.Editor.TooltipPremiumReaction" = "Subscribe to [Telegram Premium]() to use this reaction.";
"Story.Privacy.TooltipStoryArchivedChannel" = "Users will see this story on the channel page even after it expires.";
"Story.Editor.TooltipMutedWithAudio" = "Original audio will be removed";
"Story.Editor.TooltipUnmutedWithAudio" = "Original audio will be preserved";
"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.";
"WebApp.AllowWriteTitle" = "Allow Sending Messages?";
"WebApp.AllowWriteConfirmation" = "This will allow the bot **%@** to message you on Telegram.";
"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.";

View File

@ -392,11 +392,15 @@ public final class DrawingStickerEntityView: DrawingEntityView {
} }
override func onSelection() { override func onSelection() {
self.presentReactionSelection() if self.isReaction {
self.presentReactionSelection()
}
} }
func onDeselection() { func onDeselection() {
let _ = self.dismissReactionSelection() if self.isReaction {
let _ = self.dismissReactionSelection()
}
} }
private weak var reactionContextNode: ReactionContextNode? private weak var reactionContextNode: ReactionContextNode?

View File

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

View File

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

View File

@ -115,18 +115,20 @@ class ChatImageGalleryItem: GalleryItem {
let location: MessageHistoryEntryLocation? let location: MessageHistoryEntryLocation?
let translateToLanguage: String? let translateToLanguage: String?
let peerIsCopyProtected: Bool let peerIsCopyProtected: Bool
let isSecret: Bool
let displayInfoOnTop: Bool let displayInfoOnTop: Bool
let performAction: (GalleryControllerInteractionTapAction) -> Void let performAction: (GalleryControllerInteractionTapAction) -> Void
let openActionOptions: (GalleryControllerInteractionTapAction, Message) -> Void let openActionOptions: (GalleryControllerInteractionTapAction, Message) -> Void
let present: (ViewController, Any?) -> 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.context = context
self.presentationData = presentationData self.presentationData = presentationData
self.message = message self.message = message
self.location = location self.location = location
self.translateToLanguage = translateToLanguage self.translateToLanguage = translateToLanguage
self.peerIsCopyProtected = peerIsCopyProtected self.peerIsCopyProtected = peerIsCopyProtected
self.isSecret = isSecret
self.displayInfoOnTop = displayInfoOnTop self.displayInfoOnTop = displayInfoOnTop
self.performAction = performAction self.performAction = performAction
self.openActionOptions = openActionOptions self.openActionOptions = openActionOptions
@ -136,7 +138,7 @@ class ChatImageGalleryItem: GalleryItem {
func node(synchronous: Bool) -> GalleryItemNode { func node(synchronous: Bool) -> GalleryItemNode {
let node = ChatImageGalleryItemNode(context: self.context, presentationData: self.presentationData, performAction: self.performAction, openActionOptions: self.openActionOptions, present: self.present) 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 { 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 { 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)) 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 { if self.displayInfoOnTop {
node.titleContentView?.setMessage(self.message, presentationData: self.presentationData, accountPeerId: self.context.account.peerId) 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 message: Message?
private var translateToLanguage: String? private var translateToLanguage: String?
private var peerIsCopyProtected: Bool = false private var peerIsCopyProtected: Bool = false
private var isSecret: Bool = false
private let presentationData: PresentationData private let presentationData: PresentationData
private let imageNode: TransformImageNode private let imageNode: TransformImageNode
@ -328,11 +331,12 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(), size: statusSize)) 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.message = message
self.translateToLanguage = translateToLanguage self.translateToLanguage = translateToLanguage
self.peerIsCopyProtected = peerIsCopyProtected 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) self.footerContentNode.setMessage(message, displayInfo: displayInfo, translateToLanguage: translateToLanguage, peerIsCopyProtected: peerIsCopyProtected)
} }

View File

@ -60,7 +60,7 @@ private final class SecretMediaPreviewControllerNode: GalleryControllerNode {
var beginTimeAndTimeout: (Double, Double)? { var beginTimeAndTimeout: (Double, Double)? {
didSet { didSet {
if let (beginTime, timeout) = self.beginTimeAndTimeout { if let (beginTime, timeout) = self.beginTimeAndTimeout, Int32(timeout) != viewOnceTimeout {
if self.timeoutNode == nil { if self.timeoutNode == nil {
let timeoutNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5)) let timeoutNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5))
self.timeoutNode = timeoutNode self.timeoutNode = timeoutNode
@ -139,6 +139,7 @@ public final class SecretMediaPreviewController: ViewController {
private var messageView: MessageView? private var messageView: MessageView?
private var currentNodeMessageId: MessageId? private var currentNodeMessageId: MessageId?
private var currentNodeMessageIsVideo = false private var currentNodeMessageIsVideo = false
private var currentNodeMessageIsViewOnce = false
private var tempFile: TempBoxFile? private var tempFile: TempBoxFile?
private let _hiddenMedia = Promise<(MessageId, Media)?>(nil) private let _hiddenMedia = Promise<(MessageId, Media)?>(nil)
@ -263,6 +264,8 @@ public final class SecretMediaPreviewController: ViewController {
} }
if let attribute = message.autoclearAttribute { if let attribute = message.autoclearAttribute {
strongSelf.currentNodeMessageIsViewOnce = attribute.timeout == viewOnceTimeout
if let countdownBeginTime = attribute.countdownBeginTime { if let countdownBeginTime = attribute.countdownBeginTime {
if let videoDuration = videoDuration { if let videoDuration = videoDuration {
beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, videoDuration) beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, videoDuration)
@ -271,6 +274,8 @@ public final class SecretMediaPreviewController: ViewController {
} }
} }
} else if let attribute = message.autoremoveAttribute { } else if let attribute = message.autoremoveAttribute {
strongSelf.currentNodeMessageIsViewOnce = attribute.timeout == viewOnceTimeout
if let countdownBeginTime = attribute.countdownBeginTime { if let countdownBeginTime = attribute.countdownBeginTime {
if let videoDuration = videoDuration { if let videoDuration = videoDuration {
beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, videoDuration) beginTimeAndTimeout = (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970, videoDuration)
@ -284,10 +289,18 @@ public final class SecretMediaPreviewController: ViewController {
if file.isAnimated { if file.isAnimated {
strongSelf.title = strongSelf.presentationData.strings.SecretGif_Title strongSelf.title = strongSelf.presentationData.strings.SecretGif_Title
} else { } 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 { } 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 { if let beginTimeAndTimeout = beginTimeAndTimeout {
@ -478,7 +491,7 @@ public final class SecretMediaPreviewController: ViewController {
if !self.didSetReady { if !self.didSetReady {
self._ready.set(.single(true)) self._ready.set(.single(true))
} }
if !self.currentNodeMessageIsVideo { if !(self.currentNodeMessageIsVideo || self.currentNodeMessageIsViewOnce) {
self.dismiss() self.dismiss()
} }
} }

View File

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

View File

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

View File

@ -68,7 +68,6 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode {
super.init() super.init()
self.isOpaque = false self.isOpaque = false
// self.isLayerBacked = true
class DisplayLinkProxy: NSObject { class DisplayLinkProxy: NSObject {
weak var target: RadialStatusSecretTimeoutContentNode? weak var target: RadialStatusSecretTimeoutContentNode?
@ -85,7 +84,9 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode {
self.displayLink?.isPaused = true self.displayLink?.isPaused = true
self.displayLink?.add(to: RunLoop.main, forMode: .common) self.displayLink?.add(to: RunLoop.main, forMode: .common)
self.addSubnode(self.animationNode) if icon != nil {
self.addSubnode(self.animationNode)
}
} }
deinit { deinit {
@ -95,7 +96,14 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode {
override func layout() { override func layout() {
super.layout() super.layout()
self.animationNode.frame = CGRect(x: 6.0, y: 2.0, width: 36.0, height: 36.0) 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) { override func animateOut(to: RadialStatusNodeState, completion: @escaping () -> Void) {
@ -194,15 +202,15 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode {
} }
if let parameters = parameters as? RadialStatusSecretTimeoutContentNodeParameters { if let parameters = parameters as? RadialStatusSecretTimeoutContentNodeParameters {
if let icon = parameters.icon, let iconImage = icon.cgImage { // if let icon = parameters.icon, let _ = 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) // 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.saveGState()
context.translateBy(x: imageRect.midX, y: imageRect.midY) // context.translateBy(x: imageRect.midX, y: imageRect.midY)
context.scaleBy(x: 1.0, y: -1.0) // context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -imageRect.midX, y: -imageRect.midY) // context.translateBy(x: -imageRect.midX, y: -imageRect.midY)
context.draw(iconImage, in: imageRect) // context.draw(iconImage, in: imageRect)
context.restoreGState() // context.restoreGState()
} // }
let lineWidth: CGFloat let lineWidth: CGFloat
if parameters.sparks { if parameters.sparks {

View File

@ -53,16 +53,19 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
case let .messageActionBotAllowed(flags, domain, app): case let .messageActionBotAllowed(flags, domain, app):
if let domain = domain { if let domain = domain {
return TelegramMediaAction(action: .botDomainAccessGranted(domain: domain)) return TelegramMediaAction(action: .botDomainAccessGranted(domain: domain))
} else if case let .botApp(_, _, _, _, appName, _, _, _, _) = app { } else {
var appName: String?
if case let .botApp(_, _, _, _, appNameValue, _, _, _, _) = app {
appName = appNameValue
}
var type: BotSendMessageAccessGrantedType? var type: BotSendMessageAccessGrantedType?
if (flags & (1 << 3)) != 0 { if (flags & (1 << 1)) != 0 {
type = .request
} else if (flags & (1 << 1)) != 0 {
type = .attachMenu type = .attachMenu
} }
if (flags & (1 << 3)) != 0 {
type = .request
}
return TelegramMediaAction(action: .botAppAccessGranted(appName: appName, type: type)) return TelegramMediaAction(action: .botAppAccessGranted(appName: appName, type: type))
} else {
return nil
} }
case .messageActionSecureValuesSentMe: case .messageActionSecureValuesSentMe:
return nil return nil

View File

@ -56,7 +56,11 @@ public class AutoclearTimeoutMessageAttribute: MessageAttribute {
self.countdownBeginTime = countdownBeginTime self.countdownBeginTime = countdownBeginTime
if let 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 { } else {
self.automaticTimestampBasedAttribute = nil self.automaticTimestampBasedAttribute = nil
} }
@ -67,7 +71,11 @@ public class AutoclearTimeoutMessageAttribute: MessageAttribute {
self.countdownBeginTime = decoder.decodeOptionalInt32ForKey("c") self.countdownBeginTime = decoder.decodeOptionalInt32ForKey("c")
if let countdownBeginTime = self.countdownBeginTime { 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 { } else {
self.automaticTimestampBasedAttribute = nil self.automaticTimestampBasedAttribute = nil
} }

View File

@ -91,7 +91,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
case paymentSent(currency: String, totalAmount: Int64, invoiceSlug: String?, isRecurringInit: Bool, isRecurringUsed: Bool) case paymentSent(currency: String, totalAmount: Int64, invoiceSlug: String?, isRecurringInit: Bool, isRecurringUsed: Bool)
case customText(text: String, entities: [MessageTextEntity]) case customText(text: String, entities: [MessageTextEntity])
case botDomainAccessGranted(domain: String) case botDomainAccessGranted(domain: String)
case botAppAccessGranted(appName: String, type: BotSendMessageAccessGrantedType?) case botAppAccessGranted(appName: String?, type: BotSendMessageAccessGrantedType?)
case botSentSecureValues(types: [SentSecureValueType]) case botSentSecureValues(types: [SentSecureValueType])
case peerJoined case peerJoined
case phoneNumberRequest case phoneNumberRequest
@ -202,7 +202,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
self = .unknown self = .unknown
} }
case 35: case 35:
self = .botAppAccessGranted(appName: decoder.decodeStringForKey("app", orElse: ""), type: decoder.decodeOptionalInt32ForKey("atp").flatMap { BotSendMessageAccessGrantedType(rawValue: $0) }) self = .botAppAccessGranted(appName: decoder.decodeOptionalStringForKey("app"), type: decoder.decodeOptionalInt32ForKey("atp").flatMap { BotSendMessageAccessGrantedType(rawValue: $0) })
default: default:
self = .unknown self = .unknown
} }
@ -372,7 +372,11 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable {
encoder.encode(TelegramWallpaperNativeCodable(wallpaper), forKey: "wallpaper") encoder.encode(TelegramWallpaperNativeCodable(wallpaper), forKey: "wallpaper")
case let .botAppAccessGranted(appName, type): case let .botAppAccessGranted(appName, type):
encoder.encodeInt32(35, forKey: "_rawValue") encoder.encodeInt32(35, forKey: "_rawValue")
encoder.encodeString(appName, forKey: "app") if let appName = appName {
encoder.encodeString(appName, forKey: "app")
} else {
encoder.encodeNil(forKey: "app")
}
if let type = type { if let type = type {
encoder.encodeInt32(type.rawValue, forKey: "atp") encoder.encodeInt32(type.rawValue, forKey: "atp")
} else { } else {

View File

@ -83,7 +83,7 @@ func _internal_markMessageContentAsConsumedInteractively(postbox: Postbox, messa
} else if let attribute = updatedAttributes[i] as? AutoclearTimeoutMessageAttribute { } else if let attribute = updatedAttributes[i] as? AutoclearTimeoutMessageAttribute {
if attribute.countdownBeginTime == nil || attribute.countdownBeginTime == 0 { if attribute.countdownBeginTime == nil || attribute.countdownBeginTime == 0 {
var timeout = attribute.timeout var timeout = attribute.timeout
if let duration = message.secretMediaDuration { if let duration = message.secretMediaDuration, timeout != viewOnceTimeout {
timeout = max(timeout, Int32(duration)) timeout = max(timeout, Int32(duration))
} }
updatedAttributes[i] = AutoclearTimeoutMessageAttribute(timeout: timeout, countdownBeginTime: timestamp) updatedAttributes[i] = AutoclearTimeoutMessageAttribute(timeout: timeout, countdownBeginTime: timestamp)

View File

@ -629,8 +629,16 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
attributedString = stringWithAppliedEntities(text, entities: entities, baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: titleFont, linkFont: titleBoldFont, boldFont: titleBoldFont, italicFont: titleFont, boldItalicFont: titleBoldFont, fixedFont: titleFont, blockQuoteFont: titleFont, underlineLinks: false, message: message._asMessage()) attributedString = stringWithAppliedEntities(text, entities: entities, baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: titleFont, linkFont: titleBoldFont, boldFont: titleBoldFont, italicFont: titleFont, boldItalicFont: titleBoldFont, fixedFont: titleFont, blockQuoteFont: titleFont, underlineLinks: false, message: message._asMessage())
case let .botDomainAccessGranted(domain): case let .botDomainAccessGranted(domain):
attributedString = NSAttributedString(string: strings.AuthSessions_Message(domain).string, font: titleFont, textColor: primaryTextColor) attributedString = NSAttributedString(string: strings.AuthSessions_Message(domain).string, font: titleFont, textColor: primaryTextColor)
case let .botAppAccessGranted(appName, _): case let .botAppAccessGranted(appName, type):
attributedString = NSAttributedString(string: strings.AuthSessions_MessageApp(appName).string, font: titleFont, textColor: primaryTextColor) let text: String
if type == .attachMenu {
text = strings.Notification_BotWriteAllowedMenu
} else if type == .request {
text = strings.Notification_BotWriteAllowedRequest
} else {
text = strings.AuthSessions_MessageApp(appName ?? "").string
}
attributedString = NSAttributedString(string: text, font: titleFont, textColor: primaryTextColor)
case let .botSentSecureValues(types): case let .botSentSecureValues(types):
var typesString = "" var typesString = ""
var hasIdentity = false var hasIdentity = false

View File

@ -1466,6 +1466,7 @@ public class CameraScreen: ViewController {
}) })
} }
fileprivate var captureStartTimestamp: Double?
private func setupCamera() { private func setupCamera() {
guard self.camera == nil else { guard self.camera == nil else {
return return
@ -1575,6 +1576,7 @@ public class CameraScreen: ViewController {
camera.focus(at: CGPoint(x: 0.5, y: 0.5), autoFocus: true) camera.focus(at: CGPoint(x: 0.5, y: 0.5), autoFocus: true)
camera.startCapture() camera.startCapture()
self.captureStartTimestamp = CACurrentMediaTime()
self.camera = camera self.camera = camera
@ -2515,8 +2517,19 @@ public class CameraScreen: ViewController {
guard let self, !self.didStopCameraCapture else { guard let self, !self.didStopCameraCapture else {
return return
} }
self.didStopCameraCapture = true let currentTimestamp = CACurrentMediaTime()
self.node.pauseCameraCapture() 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 let resumeCameraCapture = { [weak self] in

View File

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

View File

@ -0,0 +1,123 @@
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.addSubview(self.buttonView.view)
self.buttonView.addSubnode(self.sourceView)
self.sourceView.addSubnode(self.contextContentView)
self.sourceView.activated = { [weak self] gesture, _ in
if let self, let component = self.component {
component.action(self, gesture)
}
}
self.buttonView.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
@objc private func pressed() {
self.component?.action(self, nil)
}
public func update(component: ContextReferenceButtonComponent, availableSize: CGSize, transition: Transition) -> CGSize {
self.component = component
let componentSize = self.componentView.update(
transition: transition,
component: component.content,
environment: {},
containerSize: availableSize
)
var size = componentSize
if let minSize = component.minSize {
size.width = max(size.width, minSize.width)
size.height = max(size.height, minSize.height)
}
if let componentView = self.componentView.view {
componentView.isUserInteractionEnabled = false
if componentView.superview == nil {
self.contextContentView.view.addSubview(componentView)
}
transition.setFrame(view: componentView, frame: CGRect(origin: CGPoint(x: floor((size.width - componentSize.width) / 2.0), y: floor((size.height - componentSize.height) / 2.0)), size: componentSize))
}
transition.setFrame(view: self.buttonView.view, frame: CGRect(origin: .zero, size: size))
transition.setFrame(view: self.sourceView.view, frame: CGRect(origin: .zero, size: size))
transition.setFrame(view: self.contextContentView.view, frame: CGRect(origin: .zero, size: size))
return size
}
}
public func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}

View File

@ -69,9 +69,13 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
} }
} }
private var scheduledMessageInput: MessageInputPanelComponent.SendMessageInput?
public func setCaption(_ caption: NSAttributedString?) { public func setCaption(_ caption: NSAttributedString?) {
let sendMessageInput = MessageInputPanelComponent.SendMessageInput.text(caption ?? NSAttributedString())
if let view = self.inputPanel.view as? MessageInputPanelComponent.View { if let view = self.inputPanel.view as? MessageInputPanelComponent.View {
view.setSendMessageInput(value: .text(caption ?? NSAttributedString()), updateState: true) view.setSendMessageInput(value: sendMessageInput, updateState: true)
} else {
self.scheduledMessageInput = sendMessageInput
} }
} }
@ -81,6 +85,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
} }
public func setTimeout(_ timeout: Int32) { public func setTimeout(_ timeout: Int32) {
self.dismissTimeoutTooltip()
var timeout: Int32? = timeout var timeout: Int32? = timeout
if timeout == 0 { if timeout == 0 {
timeout = nil timeout = nil
@ -142,6 +147,12 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
maxInputPanelHeight = 60.0 maxInputPanelHeight = 60.0
} }
var resetInputContents: MessageInputPanelComponent.SendMessageInput?
if let scheduledMessageInput = self.scheduledMessageInput {
resetInputContents = scheduledMessageInput
self.scheduledMessageInput = nil
}
self.inputPanel.parentState = self.state self.inputPanel.parentState = self.state
let inputPanelSize = self.inputPanel.update( let inputPanelSize = self.inputPanel.update(
transition: Transition(transition), transition: Transition(transition),
@ -156,7 +167,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
maxLength: 1024, maxLength: 1024,
queryTypes: [.mention], queryTypes: [.mention],
alwaysDarkWhenHasText: false, alwaysDarkWhenHasText: false,
resetInputContents: nil, resetInputContents: resetInputContents,
nextInputMode: { _ in nextInputMode: { _ in
return .emoji return .emoji
}, },
@ -180,9 +191,9 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
likeAction: nil, likeAction: nil,
likeOptionsAction: nil, likeOptionsAction: nil,
inputModeAction: nil, inputModeAction: nil,
timeoutAction: self.chatLocation.peerId?.namespace == Namespaces.Peer.CloudUser ? { [weak self] sourceView in timeoutAction: self.chatLocation.peerId?.namespace == Namespaces.Peer.CloudUser ? { [weak self] sourceView, gesture in
if let self { if let self {
self.presentTimeoutSetup(sourceView: sourceView) self.presentTimeoutSetup(sourceView: sourceView, gesture: gesture)
} }
} : nil, } : nil,
forwardAction: nil, forwardAction: nil,
@ -234,7 +245,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
return inputPanelSize.height - 8.0 return inputPanelSize.height - 8.0
} }
private func presentTimeoutSetup(sourceView: UIView) { private func presentTimeoutSetup(sourceView: UIView, gesture: ContextGesture?) {
self.hapticFeedback.impact(.light) self.hapticFeedback.impact(.light)
var items: [ContextMenuItem] = [] var items: [ContextMenuItem] = []
@ -250,12 +261,12 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
let currentValue = self.currentTimeout let currentValue = self.currentTimeout
let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme) let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme)
let title = "Choose how long the media will be kept after opening." let title = presentationData.strings.MediaPicker_Timer_Description
let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil 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: title, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction)))
items.append(.action(ContextMenuActionItem(text: "View Once", icon: { theme in 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 return currentValue == viewOnceTimeout ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, action: { _, a in }, action: { _, a in
a(.default) a(.default)
@ -263,31 +274,19 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
updateTimeout(viewOnceTimeout) updateTimeout(viewOnceTimeout)
}))) })))
items.append(.action(ContextMenuActionItem(text: "3 Seconds", icon: { theme in let values: [Int32] = [3, 10, 30]
return currentValue == 3 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, action: { _, a in
a(.default)
updateTimeout(3) 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)
items.append(.action(ContextMenuActionItem(text: "10 Seconds", icon: { theme in updateTimeout(value)
return currentValue == 10 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil })))
}, action: { _, a in }
a(.default)
updateTimeout(10) items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPicker_Timer_DoNotDelete, icon: { theme in
})))
items.append(.action(ContextMenuActionItem(text: "30 Seconds", icon: { theme in
return currentValue == 30 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, action: { _, a in
a(.default)
updateTimeout(30)
})))
items.append(.action(ContextMenuActionItem(text: "Do Not Delete", icon: { theme in
return currentValue == nil ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil return currentValue == nil ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
}, action: { _, a in }, action: { _, a in
a(.default) a(.default)
@ -295,34 +294,41 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
updateTimeout(nil) updateTimeout(nil)
}))) })))
let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
self.present(contextController) self.present(contextController)
} }
private weak var tooltipController: TooltipScreen? private weak var tooltipController: TooltipScreen?
private func presentTimeoutTooltip(sourceView: UIView, timeout: Int32?) {
guard let superview = self.view.superview?.superview else { private func dismissTimeoutTooltip() {
return
}
if let tooltipController = self.tooltipController { if let tooltipController = self.tooltipController {
self.tooltipController = nil self.tooltipController = nil
tooltipController.dismiss() 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 parentFrame = superview.convert(superview.bounds, to: nil)
let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0) 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 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 text: String
let iconName: String let iconName: String
if timeout == viewOnceTimeout { if timeout == viewOnceTimeout {
text = "Photo set to view once." text = isVideo ? presentationData.strings.MediaPicker_Timer_Video_ViewOnceTooltip : presentationData.strings.MediaPicker_Timer_Photo_ViewOnceTooltip
iconName = "anim_autoremove_on" iconName = "anim_autoremove_on"
} else if let timeout { } else if let timeout {
text = "Photo will be deleted in \(timeout) seconds after opening." text = isVideo ? presentationData.strings.MediaPicker_Timer_Video_TimerTooltip("\(timeout)").string : presentationData.strings.MediaPicker_Timer_Photo_TimerTooltip("\(timeout)").string
iconName = "anim_autoremove_on" iconName = "anim_autoremove_on"
} else { } else {
text = "Photo will be kept in chat." text = isVideo ? presentationData.strings.MediaPicker_Timer_Video_KeepTooltip : presentationData.strings.MediaPicker_Timer_Photo_KeepTooltip
iconName = "anim_autoremove_off" iconName = "anim_autoremove_off"
} }
@ -330,7 +336,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
account: self.context.account, account: self.context.account,
sharedContext: self.context.sharedContext, sharedContext: self.context.sharedContext,
text: .plain(text: text), text: .plain(text: text),
balancedTextLayout: true, balancedTextLayout: false,
style: .customBlur(UIColor(rgb: 0x18181a), 0.0), style: .customBlur(UIColor(rgb: 0x18181a), 0.0),
arrowStyle: .small, arrowStyle: .small,
icon: .animation(name: iconName, delay: 0.1, tintColor: nil), icon: .animation(name: iconName, delay: 0.1, tintColor: nil),

View File

@ -389,7 +389,7 @@ public final class MediaEditorVideoExport {
try? videoTrack.insertTimeRange(timeRange, of: videoAssetTrack, at: .zero) try? videoTrack.insertTimeRange(timeRange, of: videoAssetTrack, at: .zero)
if let audioAssetTrack = asset.tracks(withMediaType: .audio).first, let audioTrack = mixComposition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid) { if let audioAssetTrack = asset.tracks(withMediaType: .audio).first, let audioTrack = mixComposition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid), !self.configuration.values.videoIsMuted {
try? audioTrack.insertTimeRange(timeRange, of: audioAssetTrack, at: .zero) try? audioTrack.insertTimeRange(timeRange, of: audioAssetTrack, at: .zero)
} }
@ -488,7 +488,7 @@ public final class MediaEditorVideoExport {
} }
let audioTracks = inputAsset.tracks(withMediaType: .audio) let audioTracks = inputAsset.tracks(withMediaType: .audio)
if audioTracks.count > 0, !self.configuration.values.videoIsMuted { if audioTracks.count > 0, !self.configuration.values.videoIsMuted || self.configuration.values.audioTrack != nil {
let audioOutput = AVAssetReaderAudioMixOutput(audioTracks: audioTracks, audioSettings: nil) let audioOutput = AVAssetReaderAudioMixOutput(audioTracks: audioTracks, audioSettings: nil)
audioOutput.alwaysCopiesSampleData = false audioOutput.alwaysCopiesSampleData = false
if reader.canAdd(audioOutput) { if reader.canAdd(audioOutput) {

View File

@ -1117,7 +1117,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 { guard let self, let controller = self.environment?.controller() as? MediaEditorScreen else {
return return
} }
@ -1130,7 +1130,7 @@ final class MediaEditorScreenComponent: Component {
} else { } else {
hasPremium = false hasPremium = false
} }
controller?.presentTimeoutSetup(sourceView: view, hasPremium: hasPremium) controller?.presentTimeoutSetup(sourceView: view, gesture: gesture, hasPremium: hasPremium)
}) })
}, },
forwardAction: nil, forwardAction: nil,
@ -2778,7 +2778,22 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0) 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 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 let _ = self.mediaEditor?.values.audioTrack {
if isMuted {
text = self.presentationData.strings.Story_Editor_TooltipMutedWithAudio
} else {
text = self.presentationData.strings.Story_Editor_TooltipUnmutedWithAudio
}
} else {
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 return .ignore
}) })
self.muteTooltip = tooltipController self.muteTooltip = tooltipController
@ -2957,7 +2972,15 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
location = draft.location 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 { if let self {
let emojiFile: Signal<TelegramMediaFile?, NoError> let emojiFile: Signal<TelegramMediaFile?, NoError>
if let countryCode { if let countryCode {
@ -3035,7 +3058,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
} }
func presentAudioPicker() { func presentAudioPicker() {
self.controller?.present(legacyICloudFilePicker(theme: self.presentationData.theme, mode: .import, documentTypes: ["public.mp3"], forceDarkTheme: true, completion: { [weak self] urls in self.controller?.present(legacyICloudFilePicker(theme: self.presentationData.theme, mode: .import, documentTypes: ["public.mp3"], forceDarkTheme: true, dismissed: { [weak self] in
if let self {
Queue.mainQueue().after(0.1) {
self.mediaEditor?.play()
}
}
}, completion: { [weak self] urls in
guard let self, let mediaEditor = self.mediaEditor, !urls.isEmpty, let url = urls.first else { guard let self, let mediaEditor = self.mediaEditor, !urls.isEmpty, let url = urls.first else {
return return
} }
@ -3060,10 +3089,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
} }
self.requestUpdate(transition: .easeInOut(duration: 0.2)) self.requestUpdate(transition: .easeInOut(duration: 0.2))
Queue.mainQueue().after(0.1) {
self.mediaEditor?.play()
}
}), in: .window(.root)) }), in: .window(.root))
} }
@ -3081,7 +3106,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
action: { [weak self] f in action: { [weak self] f in
f.dismissWithResult(.default) f.dismissWithResult(.default)
if let self { if let self {
self.mediaEditor?.setAudioTrack(nil) if let mediaEditor = self.mediaEditor {
mediaEditor.setAudioTrack(nil)
if !mediaEditor.sourceIsVideo && !mediaEditor.isPlaying {
mediaEditor.play()
}
}
self.requestUpdate(transition: .easeInOut(duration: 0.25)) self.requestUpdate(transition: .easeInOut(duration: 0.25))
} }
} }
@ -3872,7 +3903,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) self.hapticFeedback.impact(.light)
var items: [ContextMenuItem] = [] var items: [ContextMenuItem] = []
@ -3893,7 +3924,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme) let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme)
let title = presentationData.strings.Story_Editor_ExpirationText let title = presentationData.strings.Story_Editor_ExpirationText
let currentValue = self.state.privacy.timeout let currentValue = self.state.privacy.timeout
let currentArchived = self.state.privacy.pin
let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil 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: title, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction)))
@ -3929,7 +3959,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
} }
}))) })))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Story_Editor_ExpirationValue(24), icon: { theme in 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 }, action: { _, a in
a(.default) a(.default)
@ -3951,7 +3981,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
} }
}))) })))
let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) let contextController = ContextController(presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
self.present(contextController, in: .window(.root)) self.present(contextController, in: .window(.root))
} }

View File

@ -37,6 +37,8 @@ swift_library(
"//submodules/AnimatedCountLabelNode", "//submodules/AnimatedCountLabelNode",
"//submodules/TelegramUI/Components/MessageInputActionButtonComponent", "//submodules/TelegramUI/Components/MessageInputActionButtonComponent",
"//submodules/SearchPeerMembers", "//submodules/SearchPeerMembers",
"//submodules/ContextUI",
"//submodules/TelegramUI/Components/ContextReferenceButtonComponent",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -18,6 +18,9 @@ import AudioToolbox
import AnimatedTextComponent import AnimatedTextComponent
import AnimatedCountLabelNode import AnimatedCountLabelNode
import MessageInputActionButtonComponent import MessageInputActionButtonComponent
import ContextReferenceButtonComponent
private let timeoutButtonTag = GenericComponentViewTag()
public final class MessageInputPanelComponent: Component { public final class MessageInputPanelComponent: Component {
public struct ContextQueryTypes: OptionSet { public struct ContextQueryTypes: OptionSet {
@ -120,7 +123,7 @@ public final class MessageInputPanelComponent: Component {
public let likeAction: (() -> Void)? public let likeAction: (() -> Void)?
public let likeOptionsAction: ((UIView, ContextGesture?) -> Void)? public let likeOptionsAction: ((UIView, ContextGesture?) -> Void)?
public let inputModeAction: (() -> Void)? public let inputModeAction: (() -> Void)?
public let timeoutAction: ((UIView) -> Void)? public let timeoutAction: ((UIView, ContextGesture?) -> Void)?
public let forwardAction: (() -> Void)? public let forwardAction: (() -> Void)?
public let moreAction: ((UIView, ContextGesture?) -> Void)? public let moreAction: ((UIView, ContextGesture?) -> Void)?
public let presentVoiceMessagesUnavailableTooltip: ((UIView) -> Void)? public let presentVoiceMessagesUnavailableTooltip: ((UIView) -> Void)?
@ -172,7 +175,7 @@ public final class MessageInputPanelComponent: Component {
likeAction: (() -> Void)?, likeAction: (() -> Void)?,
likeOptionsAction: ((UIView, ContextGesture?) -> Void)?, likeOptionsAction: ((UIView, ContextGesture?) -> Void)?,
inputModeAction: (() -> Void)?, inputModeAction: (() -> Void)?,
timeoutAction: ((UIView) -> Void)?, timeoutAction: ((UIView, ContextGesture?) -> Void)?,
forwardAction: (() -> Void)?, forwardAction: (() -> Void)?,
moreAction: ((UIView, ContextGesture?) -> Void)?, moreAction: ((UIView, ContextGesture?) -> Void)?,
presentVoiceMessagesUnavailableTooltip: ((UIView) -> Void)?, presentVoiceMessagesUnavailableTooltip: ((UIView) -> Void)?,
@ -1456,49 +1459,23 @@ public final class MessageInputPanelComponent: Component {
let accentColor = component.theme.chat.inputPanel.panelControlAccentColor let accentColor = component.theme.chat.inputPanel.panelControlAccentColor
if let timeoutAction = component.timeoutAction, let timeoutValue = component.timeoutValue { if let timeoutAction = component.timeoutAction, let timeoutValue = component.timeoutValue {
func generateIcon(value: String, selected: Bool) -> 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 selected {
context.setFillColor(accentColor.cgColor)
context.fillEllipse(in: CGRect(origin: .zero, size: size))
} else {
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)
})
}
let icon = generateIcon(value: timeoutValue, selected: component.timeoutSelected)
let timeoutButtonSize = self.timeoutButton.update( let timeoutButtonSize = self.timeoutButton.update(
transition: transition, transition: transition,
component: AnyComponent(Button( component: AnyComponent(ContextReferenceButtonComponent(
content: AnyComponent(Image(image: icon, size: CGSize(width: 20.0, height: 20.0))), content: AnyComponent(
action: { [weak self] in TimeoutContentComponent(
guard let self, let timeoutButtonView = self.timeoutButton.view else { color: .white,
return accentColor: accentColor,
} isSelected: component.timeoutSelected,
timeoutAction(timeoutButtonView) 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: {}, environment: {},
containerSize: CGSize(width: 32.0, height: 32.0) containerSize: CGSize(width: 32.0, height: 32.0)
) )

View File

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

View File

@ -345,6 +345,8 @@ final class ShareWithPeersScreenComponent: Component {
private var searchStateContext: ShareWithPeersScreen.StateContext? private var searchStateContext: ShareWithPeersScreen.StateContext?
private var searchStateDisposable: Disposable? private var searchStateDisposable: Disposable?
private let hapticFeedback = HapticFeedback()
private var effectiveStateValue: ShareWithPeersScreen.State? { private var effectiveStateValue: ShareWithPeersScreen.State? {
return self.searchStateContext?.stateValue ?? self.defaultStateValue return self.searchStateContext?.stateValue ?? self.defaultStateValue
} }
@ -501,6 +503,8 @@ final class ShareWithPeersScreenComponent: Component {
let translation = recognizer.translation(in: self) let translation = recognizer.translation(in: self)
self.dismissPanState = DismissPanState(translation: translation.y) self.dismissPanState = DismissPanState(translation: translation.y)
self.state?.updated(transition: .immediate) self.state?.updated(transition: .immediate)
self.updateModalOverlayTransition(transition: .immediate)
case .cancelled, .ended: case .cancelled, .ended:
if self.dismissPanState != nil { if self.dismissPanState != nil {
let translation = recognizer.translation(in: self) let translation = recognizer.translation(in: self)
@ -510,8 +514,13 @@ final class ShareWithPeersScreenComponent: Component {
if translation.y > 100.0 || velocity.y > 10.0 { if translation.y > 100.0 || velocity.y > 10.0 {
controller.requestDismiss() controller.requestDismiss()
Queue.mainQueue().justDispatch {
controller.updateModalStyleOverlayTransitionFactor(0.0, transition: .animated(duration: 0.3, curve: .spring))
}
} else { } else {
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) let transition = Transition(animation: .curve(duration: 0.3, curve: .spring))
self.state?.updated(transition: transition)
self.updateModalOverlayTransition(transition: transition)
} }
} }
default: default:
@ -549,7 +558,11 @@ final class ShareWithPeersScreenComponent: Component {
case .pin: case .pin:
if self.selectedOptions.contains(.pin) { if self.selectedOptions.contains(.pin) {
animationName = "anim_profileadd" animationName = "anim_profileadd"
text = presentationData.strings.Story_Privacy_TooltipStoryArchived if let peerId = self.sendAsPeerId, peerId.namespace != Namespaces.Peer.CloudUser {
text = presentationData.strings.Story_Privacy_TooltipStoryArchivedChannel
} else {
text = presentationData.strings.Story_Privacy_TooltipStoryArchived
}
} else { } else {
animationName = "anim_autoremove_on" animationName = "anim_autoremove_on"
text = presentationData.strings.Story_Privacy_TooltipStoryExpires text = presentationData.strings.Story_Privacy_TooltipStoryExpires
@ -776,7 +789,7 @@ final class ShareWithPeersScreenComponent: Component {
guard let self else { guard let self else {
return return
} }
let controller = ShareWithPeersScreen( let peersController = ShareWithPeersScreen(
context: component.context, context: component.context,
initialPrivacy: EngineStoryPrivacy(base: .nobody, additionallyIncludePeers: []), initialPrivacy: EngineStoryPrivacy(base: .nobody, additionallyIncludePeers: []),
stateContext: stateContext, stateContext: stateContext,
@ -791,10 +804,40 @@ final class ShareWithPeersScreenComponent: Component {
self.state?.updated(transition: .spring(duration: 0.4)) self.state?.updated(transition: .spring(duration: 0.4))
} }
) )
self.environment?.controller()?.push(controller) if let controller = self.environment?.controller() as? ShareWithPeersScreen {
controller.dismissAllTooltips()
controller.push(peersController)
}
}) })
} }
private func updateModalOverlayTransition(transition: Transition) {
guard let _ = self.component, let environment = self.environment, let itemLayout = self.itemLayout else {
return
}
var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset
topOffset = max(0.0, topOffset)
if let dismissPanState = self.dismissPanState {
topOffset += dismissPanState.translation
}
let topOffsetDistance: CGFloat = min(200.0, floor(itemLayout.containerSize.height * 0.25))
var topOffsetFraction = topOffset / topOffsetDistance
topOffsetFraction = max(0.0, min(1.0, topOffsetFraction))
let transitionFactor: CGFloat = 1.0 - topOffsetFraction
if let controller = environment.controller() {
Queue.mainQueue().justDispatch {
var transition = transition
if controller.modalStyleOverlayTransitionFactor.isZero && transitionFactor > 0.0, transition.animation.isImmediate {
transition = .spring(duration: 0.4)
}
controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: transition.containedViewLayoutTransition)
}
}
}
private func updateScrolling(transition: Transition) { private func updateScrolling(transition: Transition) {
guard let component = self.component, let environment = self.environment, let itemLayout = self.itemLayout else { guard let component = self.component, let environment = self.environment, let itemLayout = self.itemLayout else {
return return
@ -813,21 +856,7 @@ final class ShareWithPeersScreenComponent: Component {
var bottomAlpha: CGFloat = bottomDistance / bottomAlphaDistance var bottomAlpha: CGFloat = bottomDistance / bottomAlphaDistance
bottomAlpha = max(0.0, min(1.0, bottomAlpha)) bottomAlpha = max(0.0, min(1.0, bottomAlpha))
let topOffsetDistance: CGFloat = min(200.0, floor(itemLayout.containerSize.height * 0.25)) self.updateModalOverlayTransition(transition: transition)
self.topOffsetDistance = topOffsetDistance
var topOffsetFraction = topOffset / topOffsetDistance
topOffsetFraction = max(0.0, min(1.0, topOffsetFraction))
let transitionFactor: CGFloat = 1.0 - topOffsetFraction
if let controller = environment.controller() {
Queue.mainQueue().justDispatch {
var transition = transition
if controller.modalStyleOverlayTransitionFactor.isZero && transitionFactor > 0.0, transition.animation.isImmediate {
transition = .spring(duration: 0.4)
}
controller.updateModalStyleOverlayTransitionFactor(transitionFactor, transition: transition.containedViewLayoutTransition)
}
}
var visibleBounds = self.scrollView.bounds var visibleBounds = self.scrollView.bounds
visibleBounds.origin.y -= itemLayout.topInset visibleBounds.origin.y -= itemLayout.topInset
@ -996,9 +1025,9 @@ final class ShareWithPeersScreenComponent: Component {
let subtitle: String? let subtitle: String?
if case .user = peer { if case .user = peer {
subtitle = "personal account" subtitle = environment.strings.VoiceChat_PersonalAccount
} else { } else {
subtitle = "channel" subtitle = environment.strings.Channel_Status
} }
var isStories = false var isStories = false
@ -1037,6 +1066,7 @@ final class ShareWithPeersScreenComponent: Component {
if isStories { if isStories {
let _ = self.presentSendAsPeer() let _ = self.presentSendAsPeer()
} else { } else {
self.hapticFeedback.impact(.light)
self.environment?.controller()?.dismiss() self.environment?.controller()?.dismiss()
self.component?.peerCompletion(peer.id) self.component?.peerCompletion(peer.id)
} }

View File

@ -13213,7 +13213,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
let buttons: Signal<([AttachmentButtonType], [AttachmentButtonType], AttachmentButtonType?), NoError> let buttons: Signal<([AttachmentButtonType], [AttachmentButtonType], AttachmentButtonType?), NoError>
if !isScheduledMessages { if !isScheduledMessages && !peer.isDeleted {
buttons = self.context.engine.messages.attachMenuBots() buttons = self.context.engine.messages.attachMenuBots()
|> map { attachMenuBots in |> map { attachMenuBots in
var buttons = availableButtons var buttons = availableButtons

View File

@ -189,7 +189,7 @@ private class ExtendedMediaOverlayNode: ASDisplayNode {
var isRevealed = false var isRevealed = false
var tapped: () -> Void = {} var tapped: () -> Void = {}
init(enableAnimations: Bool) { init(hasImageOverlay: Bool, enableAnimations: Bool) {
self.blurredImageNode = TransformImageNode() self.blurredImageNode = TransformImageNode()
self.blurredImageNode.contentAnimations = [] self.blurredImageNode.contentAnimations = []
@ -212,7 +212,9 @@ private class ExtendedMediaOverlayNode: ASDisplayNode {
super.init() super.init()
self.addSubnode(self.blurredImageNode) if hasImageOverlay {
self.addSubnode(self.blurredImageNode)
}
self.addSubnode(self.dustNode) self.addSubnode(self.dustNode)
self.addSubnode(self.buttonNode) self.addSubnode(self.buttonNode)
@ -1986,7 +1988,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
} else { } else {
secretProgressIcon = PresentationResourcesChat.chatBubbleSecretMediaCompactIcon(theme) secretProgressIcon = PresentationResourcesChat.chatBubbleSecretMediaCompactIcon(theme)
} }
if isSecretMedia, let (maybeBeginTime, timeout) = secretBeginTimeAndTimeout, let beginTime = maybeBeginTime { if isSecretMedia, let (maybeBeginTime, timeout) = secretBeginTimeAndTimeout, let beginTime = maybeBeginTime, Int32(timeout) != viewOnceTimeout {
state = .secretTimeout(color: messageTheme.mediaOverlayControlColors.foregroundColor, icon: secretProgressIcon, beginTime: beginTime, timeout: timeout, sparks: true) state = .secretTimeout(color: messageTheme.mediaOverlayControlColors.foregroundColor, icon: secretProgressIcon, beginTime: beginTime, timeout: timeout, sparks: true)
backgroundColor = messageTheme.mediaDateAndStatusFillColor backgroundColor = messageTheme.mediaDateAndStatusFillColor
} else if isSecretMedia, let _ = secretProgressIcon { } else if isSecretMedia, let _ = secretProgressIcon {
@ -2043,7 +2045,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
if isSecretMedia { if isSecretMedia {
let remainingTime: Int32? let remainingTime: Int32?
if let (maybeBeginTime, timeout) = secretBeginTimeAndTimeout { if let (maybeBeginTime, timeout) = secretBeginTimeAndTimeout, Int32(timeout) != viewOnceTimeout {
if let beginTime = maybeBeginTime { if let beginTime = maybeBeginTime {
let elapsedTime = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - beginTime let elapsedTime = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - beginTime
remainingTime = Int32(max(0.0, timeout - elapsedTime)) remainingTime = Int32(max(0.0, timeout - elapsedTime))
@ -2133,7 +2135,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
if displaySpoiler { if displaySpoiler {
if self.extendedMediaOverlayNode == nil { 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 extendedMediaOverlayNode.tapped = { [weak self] in
self?.internallyVisible = true self?.internallyVisible = true
self?.updateVisibility() self?.updateVisibility()

View File

@ -1104,7 +1104,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
if result { if result {
sendEvent(true) sendEvent(true)
} else { } else {
controller.present(textAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, title: "Allow Sending Messages?", text: "Allow \(controller.botName) to send messages?", actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: { let alertController = textAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, title: self.presentationData.strings.WebApp_AllowWriteTitle, text: self.presentationData.strings.WebApp_AllowWriteConfirmation(controller.botName).string, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {
sendEvent(false) sendEvent(false)
}), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in }), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in
guard let self else { guard let self else {
@ -1115,16 +1115,23 @@ public final class WebAppController: ViewController, AttachmentContainable {
|> deliverOnMainQueue).start(completed: { |> deliverOnMainQueue).start(completed: {
sendEvent(true) sendEvent(true)
}) })
})]), in: .window(.root)) })], parseMarkdown: true)
alertController.dismissed = { byOutsideTap in
if byOutsideTap {
sendEvent(false)
}
}
controller.present(alertController, in: .window(.root))
} }
}) })
} }
fileprivate func shareAccountContact() { fileprivate func shareAccountContact() {
guard let controller = self.controller else { guard let controller = self.controller, let botId = self.controller?.botId, let botName = self.controller?.botName else {
return return
} }
let sendEvent: (Bool) -> Void = { success in let sendEvent: (Bool) -> Void = { success in
var paramsString: String var paramsString: String
if success { if success {
@ -1135,28 +1142,76 @@ public final class WebAppController: ViewController, AttachmentContainable {
self.webView?.sendEvent(name: "phone_requested", data: paramsString) self.webView?.sendEvent(name: "phone_requested", data: paramsString)
} }
let alertController = textAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, title: self.presentationData.strings.Conversation_ShareBotContactConfirmationTitle, text: self.presentationData.strings.Conversation_ShareBotContactConfirmation, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: { let context = self.context
sendEvent(false) let _ = (self.context.engine.data.get(
}), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId),
guard let self else { TelegramEngine.EngineData.Item.Peer.IsBlocked(id: botId)
)
|> deliverOnMainQueue).start(next: { [weak self, weak controller] accountPeer, isBlocked in
guard let self, let controller, let accountPeer else {
return return
} }
let _ = (self.context.account.postbox.loadedPeerWithId(self.context.account.peerId) var requiresUnblock = false
|> deliverOnMainQueue).start(next: { [weak self] peer in if case let .known(value) = isBlocked, value {
if let self, let botId = self.controller?.botId, let peer = peer as? TelegramUser, let phone = peer.phone, !phone.isEmpty { requiresUnblock = true
let _ = enqueueMessages(account: self.context.account, peerId: botId, messages: [
.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id, vCardData: nil)), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])
]).start()
sendEvent(true)
}
})
})])
alertController.dismissed = { byOutsideTap in
if byOutsideTap {
sendEvent(false)
} }
}
controller.present(alertController, in: .window(.root)) let text: String
if requiresUnblock {
text = self.presentationData.strings.WebApp_SharePhoneConfirmationUnblock(botName).string
} else {
text = self.presentationData.strings.WebApp_SharePhoneConfirmation(botName).string
}
let alertController = textAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, title: self.presentationData.strings.WebApp_SharePhoneTitle, text: text, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {
sendEvent(false)
}), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in
guard let self, case let .user(user) = accountPeer, let phone = user.phone, !phone.isEmpty else {
return
}
let sendMessageSignal = enqueueMessages(account: self.context.account, peerId: botId, messages: [
.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaContact(firstName: user.firstName ?? "", lastName: user.lastName ?? "", phoneNumber: phone, peerId: user.id, vCardData: nil)), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])
])
|> mapToSignal { messageIds in
if let maybeMessageId = messageIds.first, let messageId = maybeMessageId {
return context.account.pendingMessageManager.pendingMessageStatus(messageId)
|> mapToSignal { status, _ -> Signal<Bool, NoError> in
if status != nil {
return .never()
} else {
return .single(true)
}
}
|> take(1)
} else {
return .complete()
}
}
let sendMessage = {
let _ = (sendMessageSignal
|> deliverOnMainQueue).start(completed: {
sendEvent(true)
})
}
if requiresUnblock {
let _ = (context.engine.privacy.requestUpdatePeerIsBlocked(peerId: botId, isBlocked: false)
|> deliverOnMainQueue).start(completed: {
sendMessage()
})
} else {
sendMessage()
}
})], parseMarkdown: true)
alertController.dismissed = { byOutsideTap in
if byOutsideTap {
sendEvent(false)
}
}
controller.present(alertController, in: .window(.root))
})
} }
fileprivate func invokeCustomMethod(requestId: String, method: String, params: String) { fileprivate func invokeCustomMethod(requestId: String, method: String, params: String) {