Merge commit '23d36baeb29fc9699c88b747ed966291d05f8374'

This commit is contained in:
Ali 2023-02-10 22:19:18 +04:00
commit af88f1840b
46 changed files with 850 additions and 306 deletions

View File

@ -8910,3 +8910,12 @@ Sorry for the inconvenience.";
"Appearance.VoiceOver.Theme" = "%@ Theme"; "Appearance.VoiceOver.Theme" = "%@ Theme";
"ChatList.EmptyChatListWithArchive" = "All of your chats are archived."; "ChatList.EmptyChatListWithArchive" = "All of your chats are archived.";
"Conversation.AudioRateTooltip15X" = "Audio will play at 1.5X speed.";
"Conversation.AudioRateOptionsTooltip" = "Long tap for more speed values.";
"ImportStickerPack.EmojiCount_1" = "%@ Emoji";
"ImportStickerPack.EmojiCount_any" = "%@ Emojis";
"ImportStickerPack.ImportingEmojis" = "Importing Emojis";
"ImportStickerPack.CreateNewEmojiPack" = "Create a New Emoji Pack";

View File

@ -371,12 +371,12 @@ private final class LoadingProgressNode: ASDisplayNode {
} }
public struct AttachmentMainButtonState { public struct AttachmentMainButtonState {
let text: String? public let text: String?
let backgroundColor: UIColor public let backgroundColor: UIColor
let textColor: UIColor public let textColor: UIColor
let isVisible: Bool public let isVisible: Bool
let isLoading: Bool public let isLoading: Bool
let isEnabled: Bool public let isEnabled: Bool
public init( public init(
text: String?, text: String?,

View File

@ -2713,7 +2713,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
strongSelf.context.sharedContext.mediaManager.setPlaylist(nil, type: type, control: SharedMediaPlayerControlAction.playback(.pause)) strongSelf.context.sharedContext.mediaManager.setPlaylist(nil, type: type, control: SharedMediaPlayerControlAction.playback(.pause))
} }
} }
mediaAccessoryPanel.setRate = { [weak self] rate in mediaAccessoryPanel.setRate = { [weak self] rate, fromMenu in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
@ -2742,21 +2742,28 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
}) })
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let slowdown: Bool? let text: String?
let rate: CGFloat?
if baseRate == .x1 { if baseRate == .x1 {
slowdown = true text = presentationData.strings.Conversation_AudioRateTooltipNormal
rate = 1.0
} else if baseRate == .x1_5 {
text = presentationData.strings.Conversation_AudioRateTooltip15X
rate = 1.5
} else if baseRate == .x2 { } else if baseRate == .x2 {
slowdown = false text = presentationData.strings.Conversation_AudioRateTooltipSpeedUp
rate = 2.0
} else { } else {
slowdown = nil text = nil
rate = nil
} }
if let slowdown = slowdown { if let rate, let text, !fromMenu {
controller.present( controller.present(
UndoOverlayController( UndoOverlayController(
presentationData: presentationData, presentationData: presentationData,
content: .audioRate( content: .audioRate(
slowdown: slowdown, rate: rate,
text: slowdown ? presentationData.strings.Conversation_AudioRateTooltipNormal : presentationData.strings.Conversation_AudioRateTooltipSpeedUp text: text
), ),
elevatedLayout: false, elevatedLayout: false,
animateInAsReplacement: hasTooltip, animateInAsReplacement: hasTooltip,

View File

@ -1357,7 +1357,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
} }
strongSelf.updateVideoVisibility() strongSelf.updateVideoVisibility()
} else { } else {
let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: peer.id).start() if let photo = peer.largeProfileImage, photo.hasVideo {
let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: peer.id).start()
}
} }
})) }))
} else { } else {

View File

@ -45,6 +45,7 @@ swift_library(
"//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities", "//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities",
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache", "//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
"//submodules/TelegramUI/Components/SliderContextItem:SliderContextItem",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -22,6 +22,7 @@ import TelegramUIPreferences
import OpenInExternalAppUI import OpenInExternalAppUI
import AVKit import AVKit
import TextFormat import TextFormat
import SliderContextItem
public enum UniversalVideoGalleryItemContentInfo { public enum UniversalVideoGalleryItemContentInfo {
case message(Message) case message(Message)
@ -1267,17 +1268,11 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
strongSelf.moreBarButtonRateTimestamp = CFAbsoluteTimeGetCurrent() strongSelf.moreBarButtonRateTimestamp = CFAbsoluteTimeGetCurrent()
if abs(effectiveBaseRate - 1.0) > 0.01 { if abs(effectiveBaseRate - 1.0) > 0.01 {
let rateString: String var stringValue = String(format: "%.1fx", effectiveBaseRate)
if abs(effectiveBaseRate - 0.5) < 0.01 { if stringValue.hasSuffix(".0x") {
rateString = "0.5x" stringValue = stringValue.replacingOccurrences(of: ".0x", with: "x")
} else if abs(effectiveBaseRate - 1.5) < 0.01 {
rateString = "1.5x"
} else if abs(effectiveBaseRate - 2.0) < 0.01 {
rateString = "2x"
} else {
rateString = "x"
} }
strongSelf.moreBarButton.setContent(.image(optionsRateImage(rate: rateString, isLarge: true)), animated: animated) strongSelf.moreBarButton.setContent(.image(optionsRateImage(rate: stringValue, isLarge: true)), animated: animated)
} else { } else {
strongSelf.moreBarButton.setContent(.more(optionsCircleImage(dark: false)), animated: animated) strongSelf.moreBarButton.setContent(.more(optionsCircleImage(dark: false)), animated: animated)
} }
@ -2428,15 +2423,19 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
} }
private func openMoreMenu(sourceNode: ASDisplayNode, gesture: ContextGesture?) { private func openMoreMenu(sourceNode: ASDisplayNode, gesture: ContextGesture?) {
let items: Signal<[ContextMenuItem], NoError> = self.contextMenuMainItems()
guard let controller = self.baseNavigationController()?.topViewController as? ViewController else { guard let controller = self.baseNavigationController()?.topViewController as? ViewController else {
return return
} }
var dismissImpl: (() -> Void)?
let items: Signal<[ContextMenuItem], NoError> = self.contextMenuMainItems(dismiss: {
dismissImpl?()
})
let contextController = ContextController(account: self.context.account, presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.moreBarButton.referenceNode)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) let contextController = ContextController(account: self.context.account, presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme), source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.moreBarButton.referenceNode)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
self.isShowingContextMenuPromise.set(true) self.isShowingContextMenuPromise.set(true)
controller.presentInGlobalOverlay(contextController) controller.presentInGlobalOverlay(contextController)
dismissImpl = { [weak contextController] in
contextController?.dismiss()
}
contextController.dismissed = { [weak self] in contextController.dismissed = { [weak self] in
Queue.mainQueue().after(0.1, { Queue.mainQueue().after(0.1, {
self?.isShowingContextMenuPromise.set(false) self?.isShowingContextMenuPromise.set(false)
@ -2455,7 +2454,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
return speedList return speedList
} }
private func contextMenuMainItems() -> Signal<[ContextMenuItem], NoError> { private func contextMenuMainItems(dismiss: @escaping () -> Void) -> Signal<[ContextMenuItem], NoError> {
guard let videoNode = self.videoNode else { guard let videoNode = self.videoNode else {
return .single([]) return .single([])
} }
@ -2470,6 +2469,29 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
var items: [ContextMenuItem] = [] var items: [ContextMenuItem] = []
var speedValue: String = strongSelf.presentationData.strings.PlaybackSpeed_Normal
var speedIconText: String = "1x"
for (text, iconText, speed) in strongSelf.speedList(strings: strongSelf.presentationData.strings) {
if abs(speed - status.baseRate) < 0.01 {
speedValue = text
speedIconText = iconText
break
}
}
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PlaybackSpeed_Title, textLayout: .secondLineWithValue(speedValue), icon: { theme in
return optionsRateImage(rate: speedIconText, isLarge: false, color: theme.contextMenu.primaryColor)
}, action: { c, _ in
guard let strongSelf = self else {
c.dismiss(completion: nil)
return
}
c.setItems(strongSelf.contextMenuSpeedItems(dismiss: dismiss) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil)
})))
items.append(.separator)
if let (message, _, _) = strongSelf.contentInfo() { if let (message, _, _) = strongSelf.contentInfo() {
let context = strongSelf.context let context = strongSelf.context
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in
@ -2493,27 +2515,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}))) })))
} }
var speedValue: String = strongSelf.presentationData.strings.PlaybackSpeed_Normal
var speedIconText: String = "1x"
for (text, iconText, speed) in strongSelf.speedList(strings: strongSelf.presentationData.strings) {
if abs(speed - status.baseRate) < 0.01 {
speedValue = text
speedIconText = iconText
break
}
}
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PlaybackSpeed_Title, textLayout: .secondLineWithValue(speedValue), icon: { theme in
return optionsRateImage(rate: speedIconText, isLarge: false, color: theme.contextMenu.primaryColor)
}, action: { c, _ in
guard let strongSelf = self else {
c.dismiss(completion: nil)
return
}
c.setItems(strongSelf.contextMenuSpeedItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil)
})))
// if #available(iOS 11.0, *) { // if #available(iOS 11.0, *) {
// items.append(.action(ContextMenuActionItem(text: "AirPlay", textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/AirPlay"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in // items.append(.action(ContextMenuActionItem(text: "AirPlay", textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Media Gallery/AirPlay"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
// f(.default) // f(.default)
@ -2593,7 +2594,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
} }
} }
private func contextMenuSpeedItems() -> Signal<[ContextMenuItem], NoError> { private func contextMenuSpeedItems(dismiss: @escaping () -> Void) -> Signal<[ContextMenuItem], NoError> {
guard let videoNode = self.videoNode else { guard let videoNode = self.videoNode else {
return .single([]) return .single([])
} }
@ -2607,7 +2608,29 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
} }
var items: [ContextMenuItem] = [] var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Back, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
}, iconPosition: .left, action: { c, _ in
guard let strongSelf = self else {
c.dismiss(completion: nil)
return
}
c.setItems(strongSelf.contextMenuMainItems(dismiss: dismiss) |> map { ContextController.Items(content: .list($0)) }, minHeight: nil)
})))
items.append(.custom(SliderContextItem(minValue: 0.05, maxValue: 2.5, value: status.baseRate, valueChanged: { [weak self] newValue, finished in
guard let strongSelf = self, let videoNode = strongSelf.videoNode else {
return
}
videoNode.setBaseRate(newValue)
if finished {
dismiss()
}
}), true))
items.append(.separator)
for (text, _, rate) in strongSelf.speedList(strings: strongSelf.presentationData.strings) { for (text, _, rate) in strongSelf.speedList(strings: strongSelf.presentationData.strings) {
let isSelected = abs(status.baseRate - rate) < 0.01 let isSelected = abs(status.baseRate - rate) < 0.01
items.append(.action(ContextMenuActionItem(text: text, icon: { theme in items.append(.action(ContextMenuActionItem(text: text, icon: { theme in
@ -2631,17 +2654,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}))) })))
} }
items.append(.separator)
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Common_Back, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor)
}, iconPosition: .left, action: { c, _ in
guard let strongSelf = self else {
c.dismiss(completion: nil)
return
}
c.setItems(strongSelf.contextMenuMainItems() |> map { ContextController.Items(content: .list($0)) }, minHeight: nil)
})))
return items return items
} }
} }

View File

@ -11,18 +11,39 @@ enum StickerVerificationStatus {
public class ImportStickerPack { public class ImportStickerPack {
public enum StickerPackType { public enum StickerPackType {
case image public enum ContentType {
case animation case image
case video case animation
case video
var importType: CreateStickerSetType {
switch self { var importType: CreateStickerSetType.ContentType {
switch self {
case .image: case .image:
return .image return .image
case .animation: case .animation:
return .animation return .animation
case .video: case .video:
return .video return .video
}
}
}
case stickers(content: ContentType)
case emoji(content: ContentType, textColored: Bool)
var contentType: StickerPackType.ContentType {
switch self {
case let .stickers(content), let .emoji(content, _):
return content
}
}
var importType: CreateStickerSetType {
switch self {
case let .stickers(content):
return .stickers(content: content.importType)
case let .emoji(content, textColored):
return .emoji(content: content.importType, textColored: textColored)
} }
} }
} }
@ -51,12 +72,14 @@ public class ImportStickerPack {
let content: Content let content: Content
let emojis: [String] let emojis: [String]
let keywords: String
let uuid: UUID let uuid: UUID
var resource: MediaResource? var resource: MediaResource?
init(content: Content, emojis: [String], uuid: UUID = UUID()) { init(content: Content, emojis: [String], keywords: String, uuid: UUID = UUID()) {
self.content = content self.content = content
self.emojis = emojis self.emojis = emojis
self.keywords = keywords
self.uuid = uuid self.uuid = uuid
} }
@ -88,13 +111,25 @@ public class ImportStickerPack {
self.software = json["software"] as? String ?? "" self.software = json["software"] as? String ?? ""
let isAnimated = json["isAnimated"] as? Bool ?? false let isAnimated = json["isAnimated"] as? Bool ?? false
let isVideo = json["isVideo"] as? Bool ?? false let isVideo = json["isVideo"] as? Bool ?? false
let isEmoji = json["isEmoji"] as? Bool ?? false
let isTextColored = json["isTextColored"] as? Bool ?? false
let type: StickerPackType let type: StickerPackType
if isAnimated { if isEmoji {
type = .animation if isAnimated {
} else if isVideo { type = .emoji(content: .animation, textColored: isTextColored)
type = .video } else if isVideo {
type = .emoji(content: .video, textColored: isTextColored)
} else {
type = .emoji(content: .image, textColored: isTextColored)
}
} else { } else {
type = .image if isAnimated {
type = .stickers(content: .animation)
} else if isVideo {
type = .stickers(content: .video)
} else {
type = .stickers(content: .image)
}
} }
self.type = type self.type = type
@ -102,23 +137,23 @@ public class ImportStickerPack {
if let dataString = sticker["data"] as? String, let mimeType = sticker["mimeType"] as? String, let data = Data(base64Encoded: dataString) { if let dataString = sticker["data"] as? String, let mimeType = sticker["mimeType"] as? String, let data = Data(base64Encoded: dataString) {
var content: Sticker.Content? var content: Sticker.Content?
switch mimeType.lowercased() { switch mimeType.lowercased() {
case "image/png": case "image/png":
if case .image = type { if case .image = type.contentType {
content = .image(data) content = .image(data)
} }
case "application/x-tgsticker": case "application/x-tgsticker":
if case .animation = type { if case .animation = type.contentType {
content = .animation(data) content = .animation(data)
} }
case "video/webm", "image/webp", "image/gif": case "video/webm", "image/webp", "image/gif":
if case .video = type { if case .video = type.contentType {
content = .video(data, mimeType) content = .video(data, mimeType)
} }
default: default:
break break
} }
if let content = content { if let content = content {
return Sticker(content: content, emojis: sticker["emojis"] as? [String] ?? []) return Sticker(content: content, emojis: sticker["emojis"] as? [String] ?? [], keywords: sticker["keywords"] as? String ?? "")
} }
} }
return nil return nil

View File

@ -79,7 +79,7 @@ public final class ImportStickerPackController: ViewController, StandalonePresen
Queue.mainQueue().after(0.1) { Queue.mainQueue().after(0.1) {
self.controllerNode.updateStickerPack(self.stickerPack, verifiedStickers: Set(), declinedStickers: Set(), uploadedStickerResources: [:]) self.controllerNode.updateStickerPack(self.stickerPack, verifiedStickers: Set(), declinedStickers: Set(), uploadedStickerResources: [:])
if case .image = self.stickerPack.type { if case .image = self.stickerPack.type.contentType {
} else { } else {
let _ = (self.context.account.postbox.loadedPeerWithId(self.context.account.peerId) let _ = (self.context.account.postbox.loadedPeerWithId(self.context.account.peerId)
|> deliverOnMainQueue).start(next: { [weak self] peer in |> deliverOnMainQueue).start(next: { [weak self] peer in

View File

@ -394,19 +394,39 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
forceTitleUpdate = true forceTitleUpdate = true
} }
if let _ = self.stickerPack, self.currentItems.isEmpty || self.currentItems.count != self.pendingItems.count || self.pendingItems != self.currentItems || forceTitleUpdate { let itemsPerRow: Int
if let stickerPack = self.stickerPack, case .emoji = stickerPack.type {
itemsPerRow = 8
} else {
itemsPerRow = 4
}
if let stickerPack = self.stickerPack, self.currentItems.isEmpty || self.currentItems.count != self.pendingItems.count || self.pendingItems != self.currentItems || forceTitleUpdate {
let previousItems = self.currentItems let previousItems = self.currentItems
self.currentItems = self.pendingItems self.currentItems = self.pendingItems
let titleFont = Font.medium(20.0) let titleFont = Font.medium(20.0)
let title: String let title: String
if let _ = self.progress { if let _ = self.progress {
title = self.presentationData.strings.ImportStickerPack_ImportingStickers if case .emoji = stickerPack.type {
title = self.presentationData.strings.ImportStickerPack_ImportingEmojis
} else {
title = self.presentationData.strings.ImportStickerPack_ImportingStickers
}
} else { } else {
title = self.presentationData.strings.ImportStickerPack_StickerCount(Int32(self.currentItems.count)) if case .emoji = stickerPack.type {
title = self.presentationData.strings.ImportStickerPack_EmojiCount(Int32(self.currentItems.count))
} else {
title = self.presentationData.strings.ImportStickerPack_StickerCount(Int32(self.currentItems.count))
}
} }
self.contentTitleNode.attributedText = stringWithAppliedEntities(title, entities: [], baseColor: self.presentationData.theme.actionSheet.primaryTextColor, linkColor: self.presentationData.theme.actionSheet.controlAccentColor, baseFont: titleFont, linkFont: titleFont, boldFont: titleFont, italicFont: titleFont, boldItalicFont: titleFont, fixedFont: titleFont, blockQuoteFont: titleFont, message: nil) self.contentTitleNode.attributedText = stringWithAppliedEntities(title, entities: [], baseColor: self.presentationData.theme.actionSheet.primaryTextColor, linkColor: self.presentationData.theme.actionSheet.controlAccentColor, baseFont: titleFont, linkFont: titleFont, boldFont: titleFont, italicFont: titleFont, boldItalicFont: titleFont, fixedFont: titleFont, blockQuoteFont: titleFont, message: nil)
if case .emoji = stickerPack.type {
self.createActionButtonNode.setTitle(self.presentationData.strings.ImportStickerPack_CreateNewEmojiPack, with: Font.regular(20.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal)
} else {
self.createActionButtonNode.setTitle(self.presentationData.strings.ImportStickerPack_CreateNewStickerSet, with: Font.regular(20.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal)
}
if !forceTitleUpdate { if !forceTitleUpdate {
transaction = StickerPackPreviewGridTransaction(previousList: previousItems, list: self.currentItems, account: self.context.account, interaction: self.interaction, theme: self.presentationData.theme) transaction = StickerPackPreviewGridTransaction(previousList: previousItems, list: self.currentItems, account: self.context.account, interaction: self.interaction, theme: self.presentationData.theme)
} }
@ -422,7 +442,7 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
transition.updateFrame(node: self.contentTitleNode, frame: titleFrame) transition.updateFrame(node: self.contentTitleNode, frame: titleFrame)
transition.updateFrame(node: self.contentSeparatorNode, frame: CGRect(origin: CGPoint(x: contentContainerFrame.minX, y: self.contentBackgroundNode.frame.minY + titleAreaHeight), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel))) transition.updateFrame(node: self.contentSeparatorNode, frame: CGRect(origin: CGPoint(x: contentContainerFrame.minX, y: self.contentBackgroundNode.frame.minY + titleAreaHeight), size: CGSize(width: contentContainerFrame.size.width, height: UIScreenPixel)))
let itemsPerRow = 4
let itemWidth = floor(contentFrame.size.width / CGFloat(itemsPerRow)) let itemWidth = floor(contentFrame.size.width / CGFloat(itemsPerRow))
let rowCount = itemCount / itemsPerRow + (itemCount % itemsPerRow != 0 ? 1 : 0) let rowCount = itemCount / itemsPerRow + (itemCount % itemsPerRow != 0 ? 1 : 0)
@ -605,9 +625,9 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
if let localResource = item.stickerItem.resource { if let localResource = item.stickerItem.resource {
self.context.account.postbox.mediaBox.copyResourceData(from: localResource.id, to: resource.id) self.context.account.postbox.mediaBox.copyResourceData(from: localResource.id, to: resource.id)
} }
stickers.append(ImportSticker(resource: resource, emojis: item.stickerItem.emojis, dimensions: dimensions, mimeType: item.stickerItem.mimeType)) stickers.append(ImportSticker(resource: resource, emojis: item.stickerItem.emojis, dimensions: dimensions, mimeType: item.stickerItem.mimeType, keywords: item.stickerItem.keywords))
} else if let resource = item.stickerItem.resource { } else if let resource = item.stickerItem.resource {
stickers.append(ImportSticker(resource: resource, emojis: item.stickerItem.emojis, dimensions: dimensions, mimeType: item.stickerItem.mimeType)) stickers.append(ImportSticker(resource: resource, emojis: item.stickerItem.emojis, dimensions: dimensions, mimeType: item.stickerItem.mimeType, keywords: item.stickerItem.keywords))
} }
} }
var thumbnailSticker: ImportSticker? var thumbnailSticker: ImportSticker?
@ -618,7 +638,7 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
} }
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: thumbnail.data) self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: thumbnail.data)
thumbnailSticker = ImportSticker(resource: resource, emojis: [], dimensions: dimensions, mimeType: thumbnail.mimeType) thumbnailSticker = ImportSticker(resource: resource, emojis: [], dimensions: dimensions, mimeType: thumbnail.mimeType, keywords: thumbnail.keywords)
} }
let firstStickerItem = thumbnailSticker ?? stickers.first let firstStickerItem = thumbnailSticker ?? stickers.first
@ -636,7 +656,7 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
if let (_, _, count) = strongSelf.progress { if let (_, _, count) = strongSelf.progress {
strongSelf.progress = (1.0, count, count) strongSelf.progress = (1.0, count, count)
var animated = false var animated = false
if case .image = stickerPack.type { if case .image = stickerPack.type.contentType {
animated = true animated = true
} }
strongSelf.radialStatus.transitionToState(.progress(color: strongSelf.presentationData.theme.list.itemAccentColor, lineWidth: 6.0, value: 1.0, cancelEnabled: false, animateRotation: false), animated: animated, synchronous: true, completion: {}) strongSelf.radialStatus.transitionToState(.progress(color: strongSelf.presentationData.theme.list.itemAccentColor, lineWidth: 6.0, value: 1.0, cancelEnabled: false, animateRotation: false), animated: animated, synchronous: true, completion: {})
@ -804,7 +824,7 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
} }
self.pendingItems = updatedItems self.pendingItems = updatedItems
if case .image = stickerPack.type { if case .image = stickerPack.type.contentType {
} else { } else {
self.stickerPackReady = stickerPack.stickers.count == (verifiedStickers.count + declinedStickers.count) && updatedItems.count > 0 self.stickerPackReady = stickerPack.stickers.count == (verifiedStickers.count + declinedStickers.count) && updatedItems.count > 0
} }

View File

@ -18,7 +18,7 @@ public final class MediaPlaybackStoredState: Codable {
let container = try decoder.container(keyedBy: StringCodingKey.self) let container = try decoder.container(keyedBy: StringCodingKey.self)
self.timestamp = try container.decode(Double.self, forKey: "timestamp") self.timestamp = try container.decode(Double.self, forKey: "timestamp")
self.playbackRate = AudioPlaybackRate(rawValue: try container.decode(Int32.self, forKey: "playbackRate")) ?? .x1 self.playbackRate = AudioPlaybackRate(rawValue: try container.decode(Int32.self, forKey: "playbackRate"))
} }
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {

View File

@ -145,6 +145,9 @@ private final class SemanticStatusNodeIconContext: SemanticStatusNodeStateContex
if let iconImage = self.iconImage { if let iconImage = self.iconImage {
context.saveGState() context.saveGState()
let iconRect = CGRect(origin: CGPoint(), size: iconImage.size) let iconRect = CGRect(origin: CGPoint(), size: iconImage.size)
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
context.clip(to: iconRect, mask: iconImage.cgImage!) context.clip(to: iconRect, mask: iconImage.cgImage!)
context.fill(iconRect) context.fill(iconRect)
context.restoreGState() context.restoreGState()
@ -180,6 +183,9 @@ private final class SemanticStatusNodeIconContext: SemanticStatusNodeStateContex
if let iconImage = self.iconImage { if let iconImage = self.iconImage {
context.saveGState() context.saveGState()
let iconRect = CGRect(origin: CGPoint(), size: iconImage.size) let iconRect = CGRect(origin: CGPoint(), size: iconImage.size)
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
context.clip(to: iconRect, mask: iconImage.cgImage!) context.clip(to: iconRect, mask: iconImage.cgImage!)
context.fill(iconRect) context.fill(iconRect)
context.restoreGState() context.restoreGState()

View File

@ -379,7 +379,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-1645763991] = { return Api.InputStickerSet.parse_inputStickerSetID($0) } dict[-1645763991] = { return Api.InputStickerSet.parse_inputStickerSetID($0) }
dict[-930399486] = { return Api.InputStickerSet.parse_inputStickerSetPremiumGifts($0) } dict[-930399486] = { return Api.InputStickerSet.parse_inputStickerSetPremiumGifts($0) }
dict[-2044933984] = { return Api.InputStickerSet.parse_inputStickerSetShortName($0) } dict[-2044933984] = { return Api.InputStickerSet.parse_inputStickerSetShortName($0) }
dict[-6249322] = { return Api.InputStickerSetItem.parse_inputStickerSetItem($0) } dict[853188252] = { return Api.InputStickerSetItem.parse_inputStickerSetItem($0) }
dict[70813275] = { return Api.InputStickeredMedia.parse_inputStickeredMediaDocument($0) } dict[70813275] = { return Api.InputStickeredMedia.parse_inputStickeredMediaDocument($0) }
dict[1251549527] = { return Api.InputStickeredMedia.parse_inputStickeredMediaPhoto($0) } dict[1251549527] = { return Api.InputStickeredMedia.parse_inputStickeredMediaPhoto($0) }
dict[1634697192] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentGiftPremium($0) } dict[1634697192] = { return Api.InputStorePaymentPurpose.parse_inputStorePaymentGiftPremium($0) }

View File

@ -392,26 +392,27 @@ public extension Api {
} }
public extension Api { public extension Api {
enum InputStickerSetItem: TypeConstructorDescription { enum InputStickerSetItem: TypeConstructorDescription {
case inputStickerSetItem(flags: Int32, document: Api.InputDocument, emoji: String, maskCoords: Api.MaskCoords?) case inputStickerSetItem(flags: Int32, document: Api.InputDocument, emoji: String, maskCoords: Api.MaskCoords?, keywords: String?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self { switch self {
case .inputStickerSetItem(let flags, let document, let emoji, let maskCoords): case .inputStickerSetItem(let flags, let document, let emoji, let maskCoords, let keywords):
if boxed { if boxed {
buffer.appendInt32(-6249322) buffer.appendInt32(853188252)
} }
serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags, buffer: buffer, boxed: false)
document.serialize(buffer, true) document.serialize(buffer, true)
serializeString(emoji, buffer: buffer, boxed: false) serializeString(emoji, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {maskCoords!.serialize(buffer, true)} if Int(flags) & Int(1 << 0) != 0 {maskCoords!.serialize(buffer, true)}
if Int(flags) & Int(1 << 1) != 0 {serializeString(keywords!, buffer: buffer, boxed: false)}
break break
} }
} }
public func descriptionFields() -> (String, [(String, Any)]) { public func descriptionFields() -> (String, [(String, Any)]) {
switch self { switch self {
case .inputStickerSetItem(let flags, let document, let emoji, let maskCoords): case .inputStickerSetItem(let flags, let document, let emoji, let maskCoords, let keywords):
return ("inputStickerSetItem", [("flags", flags as Any), ("document", document as Any), ("emoji", emoji as Any), ("maskCoords", maskCoords as Any)]) return ("inputStickerSetItem", [("flags", flags as Any), ("document", document as Any), ("emoji", emoji as Any), ("maskCoords", maskCoords as Any), ("keywords", keywords as Any)])
} }
} }
@ -428,12 +429,15 @@ public extension Api {
if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() {
_4 = Api.parse(reader, signature: signature) as? Api.MaskCoords _4 = Api.parse(reader, signature: signature) as? Api.MaskCoords
} } } }
var _5: String?
if Int(_1!) & Int(1 << 1) != 0 {_5 = parseString(reader) }
let _c1 = _1 != nil let _c1 = _1 != nil
let _c2 = _2 != nil let _c2 = _2 != nil
let _c3 = _3 != nil let _c3 = _3 != nil
let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil let _c4 = (Int(_1!) & Int(1 << 0) == 0) || _4 != nil
if _c1 && _c2 && _c3 && _c4 { let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil
return Api.InputStickerSetItem.inputStickerSetItem(flags: _1!, document: _2!, emoji: _3!, maskCoords: _4) if _c1 && _c2 && _c3 && _c4 && _c5 {
return Api.InputStickerSetItem.inputStickerSetItem(flags: _1!, document: _2!, emoji: _3!, maskCoords: _4, keywords: _5)
} }
else { else {
return nil return nil

View File

@ -7919,6 +7919,25 @@ public extension Api.functions.stickers {
}) })
} }
} }
public extension Api.functions.stickers {
static func changeSticker(flags: Int32, sticker: Api.InputDocument, emoji: String?, maskCoords: Api.MaskCoords?, keywords: String?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.StickerSet>) {
let buffer = Buffer()
buffer.appendInt32(-179077444)
serializeInt32(flags, buffer: buffer, boxed: false)
sticker.serialize(buffer, true)
if Int(flags) & Int(1 << 0) != 0 {serializeString(emoji!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 1) != 0 {maskCoords!.serialize(buffer, true)}
if Int(flags) & Int(1 << 2) != 0 {serializeString(keywords!, buffer: buffer, boxed: false)}
return (FunctionDescription(name: "stickers.changeSticker", parameters: [("flags", String(describing: flags)), ("sticker", String(describing: sticker)), ("emoji", String(describing: emoji)), ("maskCoords", String(describing: maskCoords)), ("keywords", String(describing: keywords))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.StickerSet? in
let reader = BufferReader(buffer)
var result: Api.messages.StickerSet?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.messages.StickerSet
}
return result
})
}
}
public extension Api.functions.stickers { public extension Api.functions.stickers {
static func changeStickerPosition(sticker: Api.InputDocument, position: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.StickerSet>) { static func changeStickerPosition(sticker: Api.InputDocument, position: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.StickerSet>) {
let buffer = Buffer() let buffer = Buffer()
@ -7991,12 +8010,30 @@ public extension Api.functions.stickers {
} }
} }
public extension Api.functions.stickers { public extension Api.functions.stickers {
static func setStickerSetThumb(stickerset: Api.InputStickerSet, thumb: Api.InputDocument) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.StickerSet>) { static func renameStickerSet(stickerset: Api.InputStickerSet, title: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.StickerSet>) {
let buffer = Buffer() let buffer = Buffer()
buffer.appendInt32(-1707717072) buffer.appendInt32(306912256)
stickerset.serialize(buffer, true) stickerset.serialize(buffer, true)
thumb.serialize(buffer, true) serializeString(title, buffer: buffer, boxed: false)
return (FunctionDescription(name: "stickers.setStickerSetThumb", parameters: [("stickerset", String(describing: stickerset)), ("thumb", String(describing: thumb))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.StickerSet? in return (FunctionDescription(name: "stickers.renameStickerSet", parameters: [("stickerset", String(describing: stickerset)), ("title", String(describing: title))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.StickerSet? in
let reader = BufferReader(buffer)
var result: Api.messages.StickerSet?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.messages.StickerSet
}
return result
})
}
}
public extension Api.functions.stickers {
static func setStickerSetThumb(flags: Int32, stickerset: Api.InputStickerSet, thumb: Api.InputDocument?, thumbDocumentId: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.StickerSet>) {
let buffer = Buffer()
buffer.appendInt32(-1486204014)
serializeInt32(flags, buffer: buffer, boxed: false)
stickerset.serialize(buffer, true)
if Int(flags) & Int(1 << 0) != 0 {thumb!.serialize(buffer, true)}
if Int(flags) & Int(1 << 1) != 0 {serializeInt64(thumbDocumentId!, buffer: buffer, boxed: false)}
return (FunctionDescription(name: "stickers.setStickerSetThumb", parameters: [("flags", String(describing: flags)), ("stickerset", String(describing: stickerset)), ("thumb", String(describing: thumb)), ("thumbDocumentId", String(describing: thumbDocumentId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.StickerSet? in
let reader = BufferReader(buffer) let reader = BufferReader(buffer)
var result: Api.messages.StickerSet? var result: Api.messages.StickerSet?
if let signature = reader.readInt32() { if let signature = reader.readInt32() {

View File

@ -24,6 +24,9 @@ swift_library(
"//submodules/Markdown:Markdown", "//submodules/Markdown:Markdown",
"//submodules/TelegramCallsUI:TelegramCallsUI", "//submodules/TelegramCallsUI:TelegramCallsUI",
"//submodules/ManagedAnimationNode:ManagedAnimationNode", "//submodules/ManagedAnimationNode:ManagedAnimationNode",
"//submodules/TelegramNotices:TelegramNotices",
"//submodules/TooltipUI:TooltipUI",
"//submodules/TelegramUI/Components/SliderContextItem:SliderContextItem",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -11,6 +11,9 @@ import AccountContext
import TelegramStringFormatting import TelegramStringFormatting
import ManagedAnimationNode import ManagedAnimationNode
import ContextUI import ContextUI
import TelegramNotices
import TooltipUI
import SliderContextItem
private let titleFont = Font.regular(12.0) private let titleFont = Font.regular(12.0)
private let subtitleFont = Font.regular(10.0) private let subtitleFont = Font.regular(10.0)
@ -171,7 +174,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
public var tapAction: (() -> Void)? public var tapAction: (() -> Void)?
public var close: (() -> Void)? public var close: (() -> Void)?
public var setRate: ((AudioPlaybackRate) -> Void)? public var setRate: ((AudioPlaybackRate, Bool) -> Void)?
public var togglePlayPause: (() -> Void)? public var togglePlayPause: (() -> Void)?
public var playPrevious: (() -> Void)? public var playPrevious: (() -> Void)?
public var playNext: (() -> Void)? public var playNext: (() -> Void)?
@ -184,24 +187,11 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
guard self.playbackBaseRate != oldValue, let playbackBaseRate = self.playbackBaseRate else { guard self.playbackBaseRate != oldValue, let playbackBaseRate = self.playbackBaseRate else {
return return
} }
switch playbackBaseRate { self.rateButton.accessibilityLabel = self.strings.VoiceOver_Media_PlaybackRate
case .x0_5: self.rateButton.accessibilityHint = self.strings.VoiceOver_Media_PlaybackRateChange
self.rateButton.setContent(.image(optionsRateImage(rate: "0.5X", color: self.theme.rootController.navigationBar.accentTextColor))) self.rateButton.accessibilityValue = playbackBaseRate.stringValue
case .x1:
self.rateButton.setContent(.image(optionsRateImage(rate: "1X", color: self.theme.rootController.navigationBar.controlColor))) self.rateButton.setContent(.image(optionsRateImage(rate: playbackBaseRate.stringValue.uppercased(), color: self.theme.rootController.navigationBar.controlColor)))
self.rateButton.accessibilityLabel = self.strings.VoiceOver_Media_PlaybackRate
self.rateButton.accessibilityValue = self.strings.VoiceOver_Media_PlaybackRateNormal
self.rateButton.accessibilityHint = self.strings.VoiceOver_Media_PlaybackRateChange
case .x1_5:
self.rateButton.setContent(.image(optionsRateImage(rate: "1.5X", color: self.theme.rootController.navigationBar.accentTextColor)))
case .x2:
self.rateButton.setContent(.image(optionsRateImage(rate: "2X", color: self.theme.rootController.navigationBar.accentTextColor)))
self.rateButton.accessibilityLabel = self.strings.VoiceOver_Media_PlaybackRate
self.rateButton.accessibilityValue = self.strings.VoiceOver_Media_PlaybackRateFast
self.rateButton.accessibilityHint = self.strings.VoiceOver_Media_PlaybackRateChange
default:
break
}
} }
} }
@ -374,18 +364,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
self.scrubbingNode.updateContent(.standard(lineHeight: 2.0, lineCap: .square, scrubberHandle: .none, backgroundColor: .clear, foregroundColor: self.theme.rootController.navigationBar.accentTextColor, bufferingColor: self.theme.rootController.navigationBar.accentTextColor.withAlphaComponent(0.5), chapters: [])) self.scrubbingNode.updateContent(.standard(lineHeight: 2.0, lineCap: .square, scrubberHandle: .none, backgroundColor: .clear, foregroundColor: self.theme.rootController.navigationBar.accentTextColor, bufferingColor: self.theme.rootController.navigationBar.accentTextColor.withAlphaComponent(0.5), chapters: []))
if let playbackBaseRate = self.playbackBaseRate { if let playbackBaseRate = self.playbackBaseRate {
switch playbackBaseRate { self.rateButton.setContent(.image(optionsRateImage(rate: playbackBaseRate.stringValue.uppercased(), color: self.theme.rootController.navigationBar.controlColor)))
case .x0_5:
self.rateButton.setContent(.image(optionsRateImage(rate: "0.5X", color: self.theme.rootController.navigationBar.accentTextColor)))
case .x1:
self.rateButton.setContent(.image(optionsRateImage(rate: "1X", color: self.theme.rootController.navigationBar.controlColor)))
case .x1_5:
self.rateButton.setContent(.image(optionsRateImage(rate: "1.5X", color: self.theme.rootController.navigationBar.accentTextColor)))
case .x2:
self.rateButton.setContent(.image(optionsRateImage(rate: "2X", color: self.theme.rootController.navigationBar.accentTextColor)))
default:
break
}
} }
if let (size, leftInset, rightInset) = self.validLayout { if let (size, leftInset, rightInset) = self.validLayout {
self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate) self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .immediate)
@ -493,6 +472,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: bounds.size.width - 44.0 - rightInset, y: 0.0), size: CGSize(width: 44.0, height: minHeight))) transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: bounds.size.width - 44.0 - rightInset, y: 0.0), size: CGSize(width: 44.0, height: minHeight)))
let rateButtonSize = CGSize(width: 30.0, height: minHeight) let rateButtonSize = CGSize(width: 30.0, height: minHeight)
transition.updateFrame(node: self.rateButton, frame: CGRect(origin: CGPoint(x: bounds.size.width - 33.0 - closeButtonSize.width - rateButtonSize.width - rightInset, y: -4.0), size: rateButtonSize)) transition.updateFrame(node: self.rateButton, frame: CGRect(origin: CGPoint(x: bounds.size.width - 33.0 - closeButtonSize.width - rateButtonSize.width - rightInset, y: -4.0), size: rateButtonSize))
transition.updateFrame(node: self.playPauseIconNode, frame: CGRect(origin: CGPoint(x: 6.0, y: 4.0 + UIScreenPixel), size: CGSize(width: 28.0, height: 28.0))) transition.updateFrame(node: self.playPauseIconNode, frame: CGRect(origin: CGPoint(x: 6.0, y: 4.0 + UIScreenPixel), size: CGSize(width: 28.0, height: 28.0)))
transition.updateFrame(node: self.actionButton, frame: CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: 40.0, height: 37.0))) transition.updateFrame(node: self.actionButton, frame: CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: 40.0, height: 37.0)))
transition.updateFrame(node: self.scrubbingNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 37.0 - 2.0), size: CGSize(width: size.width, height: 2.0))) transition.updateFrame(node: self.scrubbingNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 37.0 - 2.0), size: CGSize(width: size.width, height: 2.0)))
@ -520,7 +500,18 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
} else { } else {
nextRate = .x2 nextRate = .x2
} }
self.setRate?(nextRate) self.setRate?(nextRate, false)
let frame = self.rateButton.view.convert(self.rateButton.bounds, to: nil)
let _ = (ApplicationSpecificNotice.incrementAudioRateOptionsTip(accountManager: self.context.sharedContext.accountManager)
|> deliverOnMainQueue).start(next: { [weak self] value in
if let strongSelf = self, let controller = strongSelf.getController?(), value == 4 {
controller.present(TooltipScreen(account: strongSelf.context.account, text: strongSelf.strings.Conversation_AudioRateOptionsTooltip, style: .default, icon: nil, location: .point(frame.offsetBy(dx: 0.0, dy: 4.0), .bottom), displayDuration: .custom(3.0), inset: 3.0, shouldDismissOnTouch: { _ in
return .dismiss(consume: false)
}), in: .window(.root))
}
})
} }
private func speedList(strings: PresentationStrings) -> [(String, String, AudioPlaybackRate)] { private func speedList(strings: PresentationStrings) -> [(String, String, AudioPlaybackRate)] {
@ -533,9 +524,18 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
return speedList return speedList
} }
private func contextMenuSpeedItems() -> Signal<[ContextMenuItem], NoError> { private func contextMenuSpeedItems(dismiss: @escaping () -> Void) -> Signal<[ContextMenuItem], NoError> {
var items: [ContextMenuItem] = [] var items: [ContextMenuItem] = []
items.append(.custom(SliderContextItem(minValue: 0.05, maxValue: 2.5, value: self.playbackBaseRate?.doubleValue ?? 1.0, valueChanged: { [weak self] newValue, finished in
self?.setRate?(AudioPlaybackRate(newValue), true)
if finished {
dismiss()
}
}), true))
items.append(.separator)
for (text, _, rate) in self.speedList(strings: self.strings) { for (text, _, rate) in self.speedList(strings: self.strings) {
let isSelected = self.playbackBaseRate == rate let isSelected = self.playbackBaseRate == rate
items.append(.action(ContextMenuActionItem(text: text, icon: { theme in items.append(.action(ContextMenuActionItem(text: text, icon: { theme in
@ -547,7 +547,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
}, action: { [weak self] _, f in }, action: { [weak self] _, f in
f(.default) f(.default)
self?.setRate?(rate) self?.setRate?(rate, true)
}))) })))
} }
@ -558,9 +558,14 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
guard let controller = self.getController?() else { guard let controller = self.getController?() else {
return return
} }
let items: Signal<[ContextMenuItem], NoError> = self.contextMenuSpeedItems() var dismissImpl: (() -> Void)?
let items: Signal<[ContextMenuItem], NoError> = self.contextMenuSpeedItems(dismiss: {
dismissImpl?()
})
let contextController = ContextController(account: self.context.account, presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.rateButton.referenceNode, shouldBeDismissed: self.dismissedPromise.get())), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) let contextController = ContextController(account: self.context.account, presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.rateButton.referenceNode, shouldBeDismissed: self.dismissedPromise.get())), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: gesture)
dismissImpl = { [weak contextController] in
contextController?.dismiss()
}
self.presentInGlobalOverlay?(contextController) self.presentInGlobalOverlay?(contextController)
} }
@ -626,41 +631,38 @@ private final class PlayPauseIconNode: ManagedAnimationNode {
} }
private func optionsRateImage(rate: String, color: UIColor = .white) -> UIImage? { private func optionsRateImage(rate: String, color: UIColor = .white) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 16.0), rotatedContext: { size, context in let isLarge = "".isEmpty
return generateImage(isLarge ? CGSize(width: 30.0, height: 30.0) : CGSize(width: 24.0, height: 24.0), rotatedContext: { size, context in
UIGraphicsPushContext(context) UIGraphicsPushContext(context)
context.clear(CGRect(origin: CGPoint(), size: size)) context.clear(CGRect(origin: CGPoint(), size: size))
let lineWidth = 1.0 + UIScreenPixel if let image = generateTintedImage(image: UIImage(bundleImageName: isLarge ? "Chat/Context Menu/Playspeed30" : "Chat/Context Menu/Playspeed24"), color: color) {
context.setLineWidth(lineWidth) image.draw(at: CGPoint(x: 0.0, y: 0.0))
context.setStrokeColor(color.cgColor) }
let string = NSMutableAttributedString(string: rate, font: Font.with(size: 11.0, design: .round, weight: .bold), textColor: color) let string = NSMutableAttributedString(string: rate, font: Font.with(size: isLarge ? 11.0 : 10.0, design: .round, weight: .semibold), textColor: color)
var offset = CGPoint(x: 1.0, y: 0.0) var offset = CGPoint(x: 1.0, y: 0.0)
var width: CGFloat
if rate.count >= 3 { if rate.count >= 3 {
if rate == "0.5X" { if rate == "0.5x" {
string.addAttribute(.kern, value: -0.8 as NSNumber, range: NSRange(string.string.startIndex ..< string.string.endIndex, in: string.string)) string.addAttribute(.kern, value: -0.8 as NSNumber, range: NSRange(string.string.startIndex ..< string.string.endIndex, in: string.string))
offset.x += -0.5 offset.x += -0.5
} else { } else {
string.addAttribute(.kern, value: -0.5 as NSNumber, range: NSRange(string.string.startIndex ..< string.string.endIndex, in: string.string)) string.addAttribute(.kern, value: -0.5 as NSNumber, range: NSRange(string.string.startIndex ..< string.string.endIndex, in: string.string))
offset.x += -0.3 offset.x += -0.3
} }
width = 29.0
} else { } else {
string.addAttribute(.kern, value: -0.5 as NSNumber, range: NSRange(string.string.startIndex ..< string.string.endIndex, in: string.string))
width = 19.0
offset.x += -0.3 offset.x += -0.3
} }
let path = UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - width) / 2.0), y: 0.0, width: width, height: 16.0).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0), byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 2.0, height: 2.0)) if !isLarge {
context.addPath(path.cgPath) offset.x *= 0.5
context.strokePath() offset.y *= 0.5
}
let boundingRect = string.boundingRect(with: size, options: [], context: nil) let boundingRect = string.boundingRect(with: size, options: [], context: nil)
string.draw(at: CGPoint(x: offset.x + floor((size.width - boundingRect.width) / 2.0), y: offset.y + UIScreenPixel + floor((size.height - boundingRect.height) / 2.0))) string.draw(at: CGPoint(x: offset.x + floor((size.width - boundingRect.width) / 2.0), y: offset.y + floor((size.height - boundingRect.height) / 2.0)))
UIGraphicsPopContext() UIGraphicsPopContext()
}) })

View File

@ -11,7 +11,7 @@ public final class MediaNavigationAccessoryPanel: ASDisplayNode {
public let containerNode: MediaNavigationAccessoryContainerNode public let containerNode: MediaNavigationAccessoryContainerNode
public var close: (() -> Void)? public var close: (() -> Void)?
public var setRate: ((AudioPlaybackRate) -> Void)? public var setRate: ((AudioPlaybackRate, Bool) -> Void)?
public var togglePlayPause: (() -> Void)? public var togglePlayPause: (() -> Void)?
public var tapAction: (() -> Void)? public var tapAction: (() -> Void)?
public var playPrevious: (() -> Void)? public var playPrevious: (() -> Void)?
@ -32,8 +32,8 @@ public final class MediaNavigationAccessoryPanel: ASDisplayNode {
close() close()
} }
} }
self.containerNode.headerNode.setRate = { [weak self] rate in self.containerNode.headerNode.setRate = { [weak self] rate, fromMenu in
self?.setRate?(rate) self?.setRate?(rate, fromMenu)
} }
self.containerNode.headerNode.togglePlayPause = { [weak self] in self.containerNode.headerNode.togglePlayPause = { [weak self] in
if let strongSelf = self, let togglePlayPause = strongSelf.togglePlayPause { if let strongSelf = self, let togglePlayPause = strongSelf.togglePlayPause {

View File

@ -669,7 +669,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
strongSelf.context.sharedContext.mediaManager.setPlaylist(nil, type: type, control: SharedMediaPlayerControlAction.playback(.pause)) strongSelf.context.sharedContext.mediaManager.setPlaylist(nil, type: type, control: SharedMediaPlayerControlAction.playback(.pause))
} }
} }
mediaAccessoryPanel.setRate = { [weak self] rate in mediaAccessoryPanel.setRate = { [weak self] rate, fromMenu in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
@ -687,41 +687,48 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
} }
strongSelf.context.sharedContext.mediaManager.playlistControl(.setBaseRate(baseRate), type: type) strongSelf.context.sharedContext.mediaManager.playlistControl(.setBaseRate(baseRate), type: type)
// var hasTooltip = false var hasTooltip = false
// strongSelf.forEachController({ controller in strongSelf.forEachController({ controller in
// if let controller = controller as? UndoOverlayController { if let controller = controller as? UndoOverlayController {
// hasTooltip = true hasTooltip = true
// controller.dismissWithCommitAction() controller.dismissWithCommitAction()
// } }
// return true return true
// }) })
//
// let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
// let slowdown: Bool? let text: String?
// if baseRate == .x1 { let rate: CGFloat?
// slowdown = true if baseRate == .x1 {
// } else if baseRate == .x2 { text = presentationData.strings.Conversation_AudioRateTooltipNormal
// slowdown = false rate = 1.0
// } else { } else if baseRate == .x1_5 {
// slowdown = nil text = presentationData.strings.Conversation_AudioRateTooltip15X
// } rate = 1.5
// if let slowdown = slowdown { } else if baseRate == .x2 {
// strongSelf.present( text = presentationData.strings.Conversation_AudioRateTooltipSpeedUp
// UndoOverlayController( rate = 2.0
// presentationData: presentationData, } else {
// content: .audioRate( text = nil
// slowdown: slowdown, rate = nil
// text: slowdown ? presentationData.strings.Conversation_AudioRateTooltipNormal : presentationData.strings.Conversation_AudioRateTooltipSpeedUp }
// ), if let rate, let text, !fromMenu {
// elevatedLayout: false, strongSelf.present(
// animateInAsReplacement: hasTooltip, UndoOverlayController(
// action: { action in presentationData: presentationData,
// return true content: .audioRate(
// } rate: rate,
// ), text: text
// in: .current ),
// ) elevatedLayout: false,
// } animateInAsReplacement: hasTooltip,
action: { action in
return true
}
),
in: .current
)
}
}) })
} }
mediaAccessoryPanel.togglePlayPause = { [weak self] in mediaAccessoryPanel.togglePlayPause = { [weak self] in

View File

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

View File

@ -81,12 +81,14 @@ public struct ImportSticker {
let emojis: [String] let emojis: [String]
public let dimensions: PixelDimensions public let dimensions: PixelDimensions
public let mimeType: String public let mimeType: String
public let keywords: String
public init(resource: MediaResource, emojis: [String], dimensions: PixelDimensions, mimeType: String) { public init(resource: MediaResource, emojis: [String], dimensions: PixelDimensions, mimeType: String, keywords: String) {
self.resource = resource self.resource = resource
self.emojis = emojis self.emojis = emojis
self.dimensions = dimensions self.dimensions = dimensions
self.mimeType = mimeType self.mimeType = mimeType
self.keywords = keywords
} }
} }
@ -96,9 +98,21 @@ public enum CreateStickerSetStatus {
} }
public enum CreateStickerSetType { public enum CreateStickerSetType {
case image public enum ContentType {
case animation case image
case video case animation
case video
}
case stickers(content: ContentType)
case emoji(content: ContentType, textColored: Bool)
var contentType: ContentType {
switch self {
case let .stickers(content), let .emoji(content, _):
return content
}
}
} }
func _internal_createStickerSet(account: Account, title: String, shortName: String, stickers: [ImportSticker], thumbnail: ImportSticker?, type: CreateStickerSetType, software: String?) -> Signal<CreateStickerSetStatus, CreateStickerSetError> { func _internal_createStickerSet(account: Account, title: String, shortName: String, stickers: [ImportSticker], thumbnail: ImportSticker?, type: CreateStickerSetType, software: String?) -> Signal<CreateStickerSetStatus, CreateStickerSetError> {
@ -133,7 +147,7 @@ func _internal_createStickerSet(account: Account, title: String, shortName: Stri
} }
if resources.count == stickers.count { if resources.count == stickers.count {
var flags: Int32 = 0 var flags: Int32 = 0
switch type { switch type.contentType {
case .animation: case .animation:
flags |= (1 << 1) flags |= (1 << 1)
case .video: case .video:
@ -141,12 +155,24 @@ func _internal_createStickerSet(account: Account, title: String, shortName: Stri
default: default:
break break
} }
if case let .emoji(_, textColored) = type {
flags |= (1 << 5)
if textColored {
flags |= (1 << 6)
}
}
var inputStickers: [Api.InputStickerSetItem] = [] var inputStickers: [Api.InputStickerSetItem] = []
let stickerDocuments = thumbnail != nil ? resources.dropLast() : resources let stickerDocuments = thumbnail != nil ? resources.dropLast() : resources
for i in 0 ..< stickerDocuments.count { for i in 0 ..< stickerDocuments.count {
let sticker = stickers[i] let sticker = stickers[i]
let resource = resources[i] let resource = resources[i]
inputStickers.append(.inputStickerSetItem(flags: 0, document: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), emoji: sticker.emojis.first ?? "", maskCoords: nil))
var flags: Int32 = 0
if sticker.keywords.count > 0 {
flags |= (1 << 1)
}
inputStickers.append(.inputStickerSetItem(flags: flags, document: .inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), emoji: sticker.emojis.first ?? "", maskCoords: nil, keywords: sticker.keywords))
} }
var thumbnailDocument: Api.InputDocument? var thumbnailDocument: Api.InputDocument?
if thumbnail != nil, let resource = resources.last { if thumbnail != nil, let resource = resources.last {

View File

@ -167,6 +167,8 @@ private enum ApplicationSpecificGlobalNotice: Int32 {
case audioTranscriptionSuggestion = 33 case audioTranscriptionSuggestion = 33
case clearStorageDismissedTipSize = 34 case clearStorageDismissedTipSize = 34
case dismissedTrendingEmojiPacks = 35 case dismissedTrendingEmojiPacks = 35
case audioRateOptionsTip = 36
case translationSuggestion = 37
var key: ValueBoxKey { var key: ValueBoxKey {
let v = ValueBoxKey(length: 4) let v = ValueBoxKey(length: 4)
@ -359,6 +361,14 @@ private struct ApplicationSpecificNoticeKeys {
static func dismissedTrendingEmojiPacks() -> NoticeEntryKey { static func dismissedTrendingEmojiPacks() -> NoticeEntryKey {
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.dismissedTrendingEmojiPacks.key) return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.dismissedTrendingEmojiPacks.key)
} }
static func translationSuggestionNotice() -> NoticeEntryKey {
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.translationSuggestion.key)
}
static func audioRateOptionsTip() -> NoticeEntryKey {
return NoticeEntryKey(namespace: noticeNamespace(namespace: globalNamespace), key: ApplicationSpecificGlobalNotice.audioRateOptionsTip.key)
}
} }
public struct ApplicationSpecificNotice { public struct ApplicationSpecificNotice {
@ -1207,22 +1217,84 @@ public struct ApplicationSpecificNotice {
} }
} }
public static func incrementAudioTranscriptionSuggestion(accountManager: AccountManager<TelegramAccountManagerTypes>, count: Int = 1) -> Signal<Int, NoError> { public static func incrementAudioTranscriptionSuggestion(accountManager: AccountManager<TelegramAccountManagerTypes>, count: Int32 = 1) -> Signal<Int32, NoError> {
return accountManager.transaction { transaction -> Int in return accountManager.transaction { transaction -> Int32 in
var currentValue: Int32 = 0 var currentValue: Int32 = 0
if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.audioTranscriptionSuggestion())?.get(ApplicationSpecificCounterNotice.self) { if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.audioTranscriptionSuggestion())?.get(ApplicationSpecificCounterNotice.self) {
currentValue = value.value currentValue = value.value
} }
let previousValue = currentValue let previousValue = currentValue
currentValue += Int32(count) currentValue += count
if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) { if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) {
transaction.setNotice(ApplicationSpecificNoticeKeys.audioTranscriptionSuggestion(), entry) transaction.setNotice(ApplicationSpecificNoticeKeys.audioTranscriptionSuggestion(), entry)
} }
return Int(previousValue) return previousValue
} }
} }
public static func translationSuggestion(accountManager: AccountManager<TelegramAccountManagerTypes>) -> Signal<(Int32, Int32), NoError> {
return accountManager.noticeEntry(key: ApplicationSpecificNoticeKeys.translationSuggestionNotice())
|> map { view -> (Int32, Int32) in
if let value = view.value?.get(ApplicationSpecificTimestampAndCounterNotice.self) {
return (value.counter, value.timestamp)
} else {
return (0, 0)
}
}
}
public static func incrementTranslationSuggestion(accountManager: AccountManager<TelegramAccountManagerTypes>, count: Int32 = 1, timestamp: Int32) -> Signal<Int32, NoError> {
return accountManager.transaction { transaction -> Int32 in
var currentValue: Int32 = 0
var currentTimestamp: Int32 = 0
if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.translationSuggestionNotice())?.get(ApplicationSpecificTimestampAndCounterNotice.self) {
currentValue = value.counter
currentTimestamp = value.timestamp
}
if currentTimestamp > timestamp {
return Int32(currentValue)
} else {
let previousValue = currentValue
currentValue = max(0, Int32(currentValue + count))
if let entry = CodableEntry(ApplicationSpecificTimestampAndCounterNotice(counter: currentValue, timestamp: timestamp)) {
transaction.setNotice(ApplicationSpecificNoticeKeys.translationSuggestionNotice(), entry)
}
return Int32(previousValue)
}
}
}
public static func getAudioRateOptionsTip(accountManager: AccountManager<TelegramAccountManagerTypes>) -> Signal<Int32, NoError> {
return accountManager.transaction { transaction -> Int32 in
if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.audioRateOptionsTip())?.get(ApplicationSpecificCounterNotice.self) {
return value.value
} else {
return 0
}
}
}
public static func incrementAudioRateOptionsTip(accountManager: AccountManager<TelegramAccountManagerTypes>, count: Int32 = 1) -> Signal<Int32, NoError> {
return accountManager.transaction { transaction -> Int32 in
var currentValue: Int32 = 0
if let value = transaction.getNotice(ApplicationSpecificNoticeKeys.audioRateOptionsTip())?.get(ApplicationSpecificCounterNotice.self) {
currentValue = value.value
}
let previousValue = currentValue
currentValue += count
if let entry = CodableEntry(ApplicationSpecificCounterNotice(value: currentValue)) {
transaction.setNotice(ApplicationSpecificNoticeKeys.audioRateOptionsTip(), entry)
}
return previousValue
}
}
public static func reset(accountManager: AccountManager<TelegramAccountManagerTypes>) -> Signal<Void, NoError> { public static func reset(accountManager: AccountManager<TelegramAccountManagerTypes>) -> Signal<Void, NoError> {
return accountManager.transaction { transaction -> Void in return accountManager.transaction { transaction -> Void in
} }

View File

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

View File

@ -0,0 +1,184 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ContextUI
import TelegramPresentationData
public final class SliderContextItem: ContextMenuCustomItem {
private let minValue: CGFloat
private let maxValue: CGFloat
private let value: CGFloat
private let valueChanged: (CGFloat, Bool) -> Void
public init(minValue: CGFloat, maxValue: CGFloat, value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) {
self.minValue = minValue
self.maxValue = maxValue
self.value = value
self.valueChanged = valueChanged
}
public func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode {
return SliderContextItemNode(presentationData: presentationData, getController: getController, minValue: self.minValue, maxValue: self.maxValue, value: self.value, valueChanged: self.valueChanged)
}
}
private let textFont = Font.regular(17.0)
private final class SliderContextItemNode: ASDisplayNode, ContextMenuCustomNode {
private var presentationData: PresentationData
private let backgroundTextNode: ImmediateTextNode
private let foregroundNode: ASDisplayNode
private let foregroundTextNode: ImmediateTextNode
let minValue: CGFloat
let maxValue: CGFloat
var value: CGFloat = 1.0 {
didSet {
self.updateValue()
}
}
private let valueChanged: (CGFloat, Bool) -> Void
private let hapticFeedback = HapticFeedback()
init(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, minValue: CGFloat, maxValue: CGFloat, value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) {
self.presentationData = presentationData
self.minValue = minValue
self.maxValue = maxValue
self.value = value
self.valueChanged = valueChanged
self.backgroundTextNode = ImmediateTextNode()
self.backgroundTextNode.isAccessibilityElement = false
self.backgroundTextNode.isUserInteractionEnabled = false
self.backgroundTextNode.displaysAsynchronously = false
self.backgroundTextNode.textAlignment = .left
self.foregroundNode = ASDisplayNode()
self.foregroundNode.clipsToBounds = true
self.foregroundNode.isAccessibilityElement = false
self.foregroundNode.backgroundColor = UIColor(rgb: 0xffffff)
self.foregroundNode.isUserInteractionEnabled = false
self.foregroundTextNode = ImmediateTextNode()
self.foregroundTextNode.isAccessibilityElement = false
self.foregroundTextNode.isUserInteractionEnabled = false
self.foregroundTextNode.displaysAsynchronously = false
self.foregroundTextNode.textAlignment = .left
super.init()
self.isUserInteractionEnabled = true
self.addSubnode(self.backgroundTextNode)
self.addSubnode(self.foregroundNode)
self.foregroundNode.addSubnode(self.foregroundTextNode)
}
override func didLoad() {
super.didLoad()
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
self.view.addGestureRecognizer(panGestureRecognizer)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
self.view.addGestureRecognizer(tapGestureRecognizer)
}
func updateTheme(presentationData: PresentationData) {
self.presentationData = presentationData
self.updateValue()
}
private func updateValue(transition: ContainedViewLayoutTransition = .immediate) {
let width = self.frame.width
let range = self.maxValue - self.minValue
let value = (self.value - self.minValue) / range
transition.updateFrameAdditive(node: self.foregroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: value * width, height: self.frame.height)))
var stringValue = String(format: "%.1fx", self.value)
if stringValue.hasSuffix(".0x") {
stringValue = stringValue.replacingOccurrences(of: ".0x", with: "x")
}
self.backgroundTextNode.attributedText = NSAttributedString(string: stringValue, font: textFont, textColor: UIColor(rgb: 0xffffff))
self.foregroundTextNode.attributedText = NSAttributedString(string: stringValue, font: textFont, textColor: UIColor(rgb: 0x000000))
let _ = self.backgroundTextNode.updateLayout(CGSize(width: 70.0, height: .greatestFiniteMagnitude))
let _ = self.foregroundTextNode.updateLayout(CGSize(width: 70.0, height: .greatestFiniteMagnitude))
}
func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) {
let valueWidth: CGFloat = 70.0
let height: CGFloat = 45.0
var textSize = self.backgroundTextNode.updateLayout(CGSize(width: valueWidth, height: .greatestFiniteMagnitude))
textSize.width = valueWidth
return (CGSize(width: height * 3.0, height: height), { size, transition in
let leftInset: CGFloat = 17.0
let textFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - textSize.height) / 2.0)), size: textSize)
transition.updateFrameAdditive(node: self.backgroundTextNode, frame: textFrame)
transition.updateFrameAdditive(node: self.foregroundTextNode, frame: textFrame)
self.updateValue(transition: transition)
})
}
@objc private func panGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
let range = self.maxValue - self.minValue
switch gestureRecognizer.state {
case .began:
break
case .changed:
let previousValue = self.value
let translation: CGFloat = gestureRecognizer.translation(in: gestureRecognizer.view).x
let delta = translation / self.bounds.width * range
self.value = max(self.minValue, min(self.maxValue, self.value + delta))
gestureRecognizer.setTranslation(CGPoint(), in: gestureRecognizer.view)
if self.value == 2.0 && previousValue != 2.0 {
self.hapticFeedback.impact(.soft)
} else if self.value == 1.0 && previousValue != 1.0 {
self.hapticFeedback.impact(.soft)
} else if self.value == 2.5 && previousValue != 2.5 {
self.hapticFeedback.impact(.soft)
} else if self.value == 0.05 && previousValue != 0.05 {
self.hapticFeedback.impact(.soft)
}
if abs(previousValue - self.value) >= 0.001 {
self.valueChanged(self.value, false)
}
case .ended:
let translation: CGFloat = gestureRecognizer.translation(in: gestureRecognizer.view).x
let delta = translation / self.bounds.width * range
self.value = max(self.minValue, min(self.maxValue, self.value + delta))
self.valueChanged(self.value, true)
default:
break
}
}
@objc private func tapGesture(_ gestureRecognizer: UITapGestureRecognizer) {
let range = self.maxValue - self.minValue
let location = gestureRecognizer.location(in: gestureRecognizer.view)
self.value = max(self.minValue, min(self.maxValue, location.x / range))
self.valueChanged(self.value, true)
}
func canBeHighlighted() -> Bool {
return false
}
func updateIsHighlighted(isHighlighted: Bool) {
}
func performAction() {
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -174,7 +174,9 @@ final class ChatAvatarNavigationNode: ASDisplayNode {
} }
strongSelf.updateVideoVisibility() strongSelf.updateVideoVisibility()
} else { } else {
let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: peer.id).start() if let photo = peer.largeProfileImage, photo.hasVideo {
let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: peer.id).start()
}
} }
})) }))
} else { } else {

View File

@ -428,7 +428,7 @@ final class ChatBotInfoItemNode: ListViewItemNode {
} }
return .url(url: url, concealed: concealed) return .url(url: url, concealed: concealed)
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
return .peerMention(peerMention.peerId, peerMention.mention) return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false)
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
return .textMention(peerName) return .textMention(peerName)
} else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String { } else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String {
@ -455,7 +455,7 @@ final class ChatBotInfoItemNode: ListViewItemNode {
break break
case let .url(url, concealed): case let .url(url, concealed):
self.item?.controllerInteraction.openUrl(url, concealed, nil, nil) self.item?.controllerInteraction.openUrl(url, concealed, nil, nil)
case let .peerMention(peerId, _): case let .peerMention(peerId, _, _):
if let item = self.item { if let item = self.item {
let _ = (item.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) let _ = (item.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in |> deliverOnMainQueue).start(next: { [weak self] peer in
@ -481,7 +481,7 @@ final class ChatBotInfoItemNode: ListViewItemNode {
break break
case let .url(url, _): case let .url(url, _):
item.controllerInteraction.longTap(.url(url), nil) item.controllerInteraction.longTap(.url(url), nil)
case let .peerMention(peerId, mention): case let .peerMention(peerId, mention, _):
item.controllerInteraction.longTap(.peerMention(peerId, mention), nil) item.controllerInteraction.longTap(.peerMention(peerId, mention), nil)
case let .textMention(name): case let .textMention(name):
item.controllerInteraction.longTap(.mention(name), nil) item.controllerInteraction.longTap(.mention(name), nil)

View File

@ -3550,7 +3550,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let f = { let f = {
let _ = (context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings]) let _ = (context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings])
|> take(1) |> take(1)
|> deliverOnMainQueue).start(next: { sharedData in |> deliverOnMainQueue).start(next: { [weak self] sharedData in
guard let strongSelf = self else {
return
}
let translationSettings: TranslationSettings let translationSettings: TranslationSettings
if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) { if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) {
translationSettings = current translationSettings = current
@ -3565,6 +3568,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let (_, language) = canTranslateText(context: context, text: text.string, showTranslate: translationSettings.showTranslate, showTranslateIfTopical: showTranslateIfTopical, ignoredLanguages: translationSettings.ignoredLanguages) let (_, language) = canTranslateText(context: context, text: text.string, showTranslate: translationSettings.showTranslate, showTranslateIfTopical: showTranslateIfTopical, ignoredLanguages: translationSettings.ignoredLanguages)
let _ = ApplicationSpecificNotice.incrementTranslationSuggestion(accountManager: context.sharedContext.accountManager, timestamp: Int32(Date().timeIntervalSince1970)).start()
let controller = TranslateScreen(context: context, text: text.string, canCopy: canCopy, fromLanguage: language) let controller = TranslateScreen(context: context, text: text.string, canCopy: canCopy, fromLanguage: language)
controller.pushController = { [weak self] c in controller.pushController = { [weak self] c in
self?.effectiveNavigationController?._keepModalDismissProgress = true self?.effectiveNavigationController?._keepModalDismissProgress = true
@ -6717,9 +6722,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.translationStateDisposable = (combineLatest( self.translationStateDisposable = (combineLatest(
queue: .concurrentDefaultQueue(), queue: .concurrentDefaultQueue(),
isPremium, isPremium,
isHidden isHidden,
) |> mapToSignal { isPremium, isHidden -> Signal<ChatPresentationTranslationState?, NoError> in ApplicationSpecificNotice.translationSuggestion(accountManager: self.context.sharedContext.accountManager)
if isPremium && !isHidden { ) |> mapToSignal { isPremium, isHidden, counterAndTimestamp -> Signal<ChatPresentationTranslationState?, NoError> in
var maybeSuggestPremium = false
if counterAndTimestamp.0 >= 3 {
maybeSuggestPremium = true
}
if (isPremium || maybeSuggestPremium) && !isHidden {
return chatTranslationState(context: context, peerId: peerId) return chatTranslationState(context: context, peerId: peerId)
|> map { translationState -> ChatPresentationTranslationState? in |> map { translationState -> ChatPresentationTranslationState? in
if let translationState, !translationState.fromLang.isEmpty { if let translationState, !translationState.fromLang.isEmpty {

View File

@ -475,7 +475,7 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
} }
return .url(url: url, concealed: concealed) return .url(url: url, concealed: concealed)
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
return .peerMention(peerMention.peerId, peerMention.mention) return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: true)
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
return .textMention(peerName) return .textMention(peerName)
} else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String { } else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String {

View File

@ -1163,7 +1163,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
} }
return .url(url: url, concealed: concealed) return .url(url: url, concealed: concealed)
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
return .peerMention(peerMention.peerId, peerMention.mention) return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false)
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
return .textMention(peerName) return .textMention(peerName)
} else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String { } else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String {

View File

@ -109,7 +109,7 @@ enum ChatMessageBubbleContentTapAction {
case none case none
case url(url: String, concealed: Bool) case url(url: String, concealed: Bool)
case textMention(String) case textMention(String)
case peerMention(PeerId, String) case peerMention(peerId: PeerId, mention: String, openProfile: Bool)
case botCommand(String) case botCommand(String)
case hashtag(String?, String) case hashtag(String?, String)
case instantPage case instantPage

View File

@ -3650,13 +3650,13 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
return .action({ return .action({
self.item?.controllerInteraction.openUrl(url, concealed, nil, self.item?.content.firstMessage) self.item?.controllerInteraction.openUrl(url, concealed, nil, self.item?.content.firstMessage)
}) })
case let .peerMention(peerId, _): case let .peerMention(peerId, _, openProfile):
return .action({ [weak self] in return .action({ [weak self] in
if let item = self?.item { if let item = self?.item {
let _ = (item.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) let _ = (item.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|> deliverOnMainQueue).start(next: { peer in |> deliverOnMainQueue).start(next: { peer in
if let self = self, let item = self.item, let peer = peer { if let self = self, let item = self.item, let peer = peer {
item.controllerInteraction.openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default) item.controllerInteraction.openPeer(peer, openProfile ? .info : .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default)
} }
}) })
} }
@ -3791,7 +3791,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
return .action({ return .action({
item.controllerInteraction.longTap(.url(url), message) item.controllerInteraction.longTap(.url(url), message)
}) })
case let .peerMention(peerId, mention): case let .peerMention(peerId, mention, _):
return .action({ return .action({
item.controllerInteraction.longTap(.peerMention(peerId, mention), message) item.controllerInteraction.longTap(.peerMention(peerId, mention), message)
}) })

View File

@ -586,50 +586,10 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode {
} }
strongSelf.updateVideoVisibility() strongSelf.updateVideoVisibility()
} else { } else {
let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: peer.id).start() if let photo = peer.largeProfileImage, photo.hasVideo {
let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: peer.id).start()
}
} }
// let cachedPeerData = peerView.cachedData
// if let cachedPeerData = cachedPeerData as? CachedUserData, case let .known(maybePhoto) = cachedPeerData.photo {
// if let photo = maybePhoto, let video = photo.videoRepresentations.last, let peerReference = PeerReference(peer) {
// let videoId = photo.id?.id ?? peer.id.id._internalGetInt64Value()
// let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: photo.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [])]))
// let videoContent = NativeVideoContent(id: .profileVideo(videoId, "\(Int32.random(in: 0 ..< Int32.max))"), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: false, storeAfterDownload: nil)
// if videoContent.id != strongSelf.videoContent?.id {
// strongSelf.videoNode?.removeFromSupernode()
// strongSelf.videoContent = videoContent
// }
//
// if strongSelf.hierarchyTrackingLayer == nil {
// let hierarchyTrackingLayer = HierarchyTrackingLayer()
// hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in
// guard let strongSelf = self else {
// return
// }
// strongSelf.trackingIsInHierarchy = true
// }
//
// hierarchyTrackingLayer.didExitHierarchy = { [weak self] in
// guard let strongSelf = self else {
// return
// }
// strongSelf.trackingIsInHierarchy = false
// }
// strongSelf.hierarchyTrackingLayer = hierarchyTrackingLayer
// strongSelf.layer.addSublayer(hierarchyTrackingLayer)
// }
// } else {
// strongSelf.videoContent = nil
//
// strongSelf.hierarchyTrackingLayer?.removeFromSuperlayer()
// strongSelf.hierarchyTrackingLayer = nil
// }
//
// strongSelf.updateVideoVisibility()
// } else {
})) }))
} else { } else {
self.cachedDataDisposable.set(nil) self.cachedDataDisposable.set(nil)

View File

@ -416,7 +416,7 @@ class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
} }
return .url(url: url, concealed: concealed) return .url(url: url, concealed: concealed)
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
return .peerMention(peerMention.peerId, peerMention.mention) return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false)
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
return .textMention(peerName) return .textMention(peerName)
} else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String { } else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String {

View File

@ -1703,7 +1703,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
} }
return .url(url: url, concealed: concealed) return .url(url: url, concealed: concealed)
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
return .peerMention(peerMention.peerId, peerMention.mention) return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false)
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
return .textMention(peerName) return .textMention(peerName)
} else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String { } else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String {

View File

@ -582,7 +582,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
} }
return .url(url: url, concealed: concealed) return .url(url: url, concealed: concealed)
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
return .peerMention(peerMention.peerId, peerMention.mention) return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false)
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
return .textMention(peerName) return .textMention(peerName)
} else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String { } else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String {

View File

@ -16,6 +16,8 @@ import MoreButtonNode
import ContextUI import ContextUI
import TranslateUI import TranslateUI
import TelegramUIPreferences import TelegramUIPreferences
import TelegramNotices
import PremiumUI
final class ChatTranslationPanelNode: ASDisplayNode { final class ChatTranslationPanelNode: ASDisplayNode {
private let context: AccountContext private let context: AccountContext
@ -26,7 +28,8 @@ final class ChatTranslationPanelNode: ASDisplayNode {
private let buttonIconNode: ASImageNode private let buttonIconNode: ASImageNode
private let buttonTextNode: ImmediateTextNode private let buttonTextNode: ImmediateTextNode
private let moreButton: MoreButtonNode private let moreButton: MoreButtonNode
private let closeButton: HighlightableButtonNode
private var theme: PresentationTheme? private var theme: PresentationTheme?
private var chatInterfaceState: ChatPresentationInterfaceState? private var chatInterfaceState: ChatPresentationInterfaceState?
@ -47,6 +50,11 @@ final class ChatTranslationPanelNode: ASDisplayNode {
self.moreButton = MoreButtonNode(theme: context.sharedContext.currentPresentationData.with { $0 }.theme) self.moreButton = MoreButtonNode(theme: context.sharedContext.currentPresentationData.with { $0 }.theme)
self.moreButton.iconNode.enqueueState(.more, animated: false) self.moreButton.iconNode.enqueueState(.more, animated: false)
self.moreButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0)
self.closeButton = HighlightableButtonNode()
self.closeButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0)
self.closeButton.displaysAsynchronously = false
super.init() super.init()
@ -65,6 +73,9 @@ final class ChatTranslationPanelNode: ASDisplayNode {
strongSelf.morePressed(node: strongSelf.moreButton.contextSourceNode, gesture: gesture) strongSelf.morePressed(node: strongSelf.moreButton.contextSourceNode, gesture: gesture)
} }
} }
self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside])
self.addSubnode(self.closeButton)
} }
func animateOut() { func animateOut() {
@ -90,6 +101,7 @@ final class ChatTranslationPanelNode: ASDisplayNode {
self.buttonIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Title Panels/Translate"), color: interfaceState.theme.chat.inputPanel.panelControlAccentColor) self.buttonIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Title Panels/Translate"), color: interfaceState.theme.chat.inputPanel.panelControlAccentColor)
self.moreButton.theme = interfaceState.theme self.moreButton.theme = interfaceState.theme
self.separatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor self.separatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor
self.closeButton.setImage(PresentationResourcesChat.chatInputPanelEncircledCloseIconImage(interfaceState.theme), for: [])
} }
if themeUpdated || isEnabledUpdated { if themeUpdated || isEnabledUpdated {
@ -139,6 +151,17 @@ final class ChatTranslationPanelNode: ASDisplayNode {
let moreButtonSize = self.moreButton.measure(CGSize(width: 100.0, height: panelHeight)) let moreButtonSize = self.moreButton.measure(CGSize(width: 100.0, height: panelHeight))
self.moreButton.frame = CGRect(origin: CGPoint(x: width - contentRightInset - moreButtonSize.width, y: floorToScreenPixels((panelHeight - moreButtonSize.height) / 2.0)), size: moreButtonSize) self.moreButton.frame = CGRect(origin: CGPoint(x: width - contentRightInset - moreButtonSize.width, y: floorToScreenPixels((panelHeight - moreButtonSize.height) / 2.0)), size: moreButtonSize)
let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0))
self.closeButton.frame = CGRect(origin: CGPoint(x: width - contentRightInset - closeButtonSize.width, y: floorToScreenPixels((panelHeight - closeButtonSize.height) / 2.0)), size: closeButtonSize)
if interfaceState.isPremium {
self.moreButton.isHidden = false
self.closeButton.isHidden = true
} else {
self.moreButton.isHidden = true
self.closeButton.isHidden = false
}
let buttonPadding: CGFloat = 10.0 let buttonPadding: CGFloat = 10.0
let buttonSpacing: CGFloat = 10.0 let buttonSpacing: CGFloat = 10.0
let buttonTextSize = self.buttonTextNode.updateLayout(CGSize(width: width - contentRightInset - moreButtonSize.width, height: panelHeight)) let buttonTextSize = self.buttonTextNode.updateLayout(CGSize(width: width - contentRightInset - moreButtonSize.width, height: panelHeight))
@ -154,12 +177,30 @@ final class ChatTranslationPanelNode: ASDisplayNode {
return panelHeight return panelHeight
} }
@objc private func closePressed() {
let _ = ApplicationSpecificNotice.incrementTranslationSuggestion(accountManager: self.context.sharedContext.accountManager, count: -100, timestamp: Int32(Date().timeIntervalSince1970) + 60 * 60 * 24 * 7).start()
}
@objc private func buttonPressed() { @objc private func buttonPressed() {
guard let translationState = self.chatInterfaceState?.translationState else { guard let translationState = self.chatInterfaceState?.translationState else {
return return
} }
self.interfaceInteraction?.toggleTranslation(translationState.isEnabled ? .original : .translated) let isPremium = self.chatInterfaceState?.isPremium ?? false
if isPremium {
self.interfaceInteraction?.toggleTranslation(translationState.isEnabled ? .original : .translated)
} else if !translationState.isEnabled {
let context = self.context
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumDemoScreen(context: context, subject: .translation, action: {
let controller = PremiumIntroScreen(context: context, source: .translation)
replaceImpl?(controller)
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
self.interfaceInteraction?.chatController()?.push(controller)
}
} }
@objc private func morePressed(node: ContextReferenceContentNode, gesture: ContextGesture?) { @objc private func morePressed(node: ContextReferenceContentNode, gesture: ContextGesture?) {

View File

@ -36,7 +36,7 @@ private func generateCollapseIcon(theme: PresentationTheme) -> UIImage? {
} }
private func optionsRateImage(rate: String, color: UIColor = .white) -> UIImage? { private func optionsRateImage(rate: String, color: UIColor = .white) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 16.0), rotatedContext: { size, context in return generateImage(CGSize(width: 36.0, height: 16.0), rotatedContext: { size, context in
UIGraphicsPushContext(context) UIGraphicsPushContext(context)
context.clear(CGRect(origin: CGPoint(), size: size)) context.clear(CGRect(origin: CGPoint(), size: size))
@ -50,7 +50,11 @@ private func optionsRateImage(rate: String, color: UIColor = .white) -> UIImage?
var offset = CGPoint(x: 1.0, y: 0.0) var offset = CGPoint(x: 1.0, y: 0.0)
var width: CGFloat var width: CGFloat
if rate.count >= 3 { if rate.count >= 5 {
string.addAttribute(.kern, value: -0.8 as NSNumber, range: NSRange(string.string.startIndex ..< string.string.endIndex, in: string.string))
offset.x += -0.5
width = 33.0
} else if rate.count >= 3 {
if rate == "0.5X" { if rate == "0.5X" {
string.addAttribute(.kern, value: -0.8 as NSNumber, range: NSRange(string.string.startIndex ..< string.string.endIndex, in: string.string)) string.addAttribute(.kern, value: -0.8 as NSNumber, range: NSRange(string.string.startIndex ..< string.string.endIndex, in: string.string))
offset.x += -0.5 offset.x += -0.5
@ -420,6 +424,8 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
baseRate = .x2 baseRate = .x2
} else if value.status.baseRate.isEqual(to: 1.5) { } else if value.status.baseRate.isEqual(to: 1.5) {
baseRate = .x1_5 baseRate = .x1_5
} else if value.status.baseRate.isEqual(to: 0.5) {
baseRate = .x0_5
} else { } else {
baseRate = .x1 baseRate = .x1
} }
@ -786,6 +792,8 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
self.rateButton.setImage(optionsRateImage(rate: "2X", color: self.presentationData.theme.list.itemAccentColor), for: []) self.rateButton.setImage(optionsRateImage(rate: "2X", color: self.presentationData.theme.list.itemAccentColor), for: [])
case .x1_5: case .x1_5:
self.rateButton.setImage(optionsRateImage(rate: "1.5X", color: self.presentationData.theme.list.itemAccentColor), for: []) self.rateButton.setImage(optionsRateImage(rate: "1.5X", color: self.presentationData.theme.list.itemAccentColor), for: [])
case .x0_5:
self.rateButton.setImage(optionsRateImage(rate: "0.5X", color: self.presentationData.theme.list.itemAccentColor), for: [])
default: default:
self.rateButton.setImage(optionsRateImage(rate: "1X", color: self.presentationData.theme.list.itemSecondaryTextColor), for: []) self.rateButton.setImage(optionsRateImage(rate: "1X", color: self.presentationData.theme.list.itemSecondaryTextColor), for: [])
} }

View File

@ -262,7 +262,7 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
strongSelf.context.sharedContext.mediaManager.setPlaylist(nil, type: type, control: SharedMediaPlayerControlAction.playback(.pause)) strongSelf.context.sharedContext.mediaManager.setPlaylist(nil, type: type, control: SharedMediaPlayerControlAction.playback(.pause))
} }
} }
mediaAccessoryPanel.setRate = { [weak self] rate in mediaAccessoryPanel.setRate = { [weak self] rate, fromMenu in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
@ -291,21 +291,28 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
}) })
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let slowdown: Bool? let text: String?
let rate: CGFloat?
if baseRate == .x1 { if baseRate == .x1 {
slowdown = true text = presentationData.strings.Conversation_AudioRateTooltipNormal
rate = 1.0
} else if baseRate == .x1_5 {
text = presentationData.strings.Conversation_AudioRateTooltip15X
rate = 1.5
} else if baseRate == .x2 { } else if baseRate == .x2 {
slowdown = false text = presentationData.strings.Conversation_AudioRateTooltipSpeedUp
rate = 2.0
} else { } else {
slowdown = nil text = nil
rate = nil
} }
if let slowdown = slowdown { if let rate, let text, !fromMenu {
controller.present( controller.present(
UndoOverlayController( UndoOverlayController(
presentationData: presentationData, presentationData: presentationData,
content: .audioRate( content: .audioRate(
slowdown: slowdown, rate: rate,
text: slowdown ? presentationData.strings.Conversation_AudioRateTooltipNormal : presentationData.strings.Conversation_AudioRateTooltipSpeedUp text: text
), ),
elevatedLayout: false, elevatedLayout: false,
animateInAsReplacement: hasTooltip, animateInAsReplacement: hasTooltip,

View File

@ -2821,7 +2821,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
} }
titleStringText = title titleStringText = title
titleAttributes = MultiScaleTextState.Attributes(font: Font.regular(30.0), color: presentationData.theme.list.itemPrimaryTextColor) titleAttributes = MultiScaleTextState.Attributes(font: Font.medium(30.0), color: presentationData.theme.list.itemPrimaryTextColor)
smallTitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(30.0), color: .white) smallTitleAttributes = MultiScaleTextState.Attributes(font: Font.regular(30.0), color: .white)
if self.isSettings, let user = peer as? TelegramUser { if self.isSettings, let user = peer as? TelegramUser {

View File

@ -15,26 +15,73 @@ public enum MusicPlaybackSettingsLooping: Int32 {
case all = 2 case all = 2
} }
public enum AudioPlaybackRate: Int32 { public enum AudioPlaybackRate: Equatable {
case x0_5 = 500 case x0_5
case x1 = 1000 case x1
case x1_5 = 1500 case x1_5
case x2 = 2000 case x2
case x4 = 4000 case x4
case x8 = 8000 case x8
case x16 = 16000 case x16
case custom(Int32)
public var doubleValue: Double { public var doubleValue: Double {
return Double(self.rawValue) / 1000.0 return Double(self.rawValue) / 1000.0
} }
public var rawValue: Int32 {
switch self {
case .x0_5:
return 500
case .x1:
return 1000
case .x1_5:
return 1500
case .x2:
return 2000
case .x4:
return 4000
case .x8:
return 8000
case .x16:
return 16000
case let .custom(value):
return value
}
}
public init(_ value: Double) { public init(_ value: Double) {
if let resolved = AudioPlaybackRate(rawValue: Int32(value * 1000.0)) { self.init(rawValue: Int32(value * 1000.0))
self = resolved }
} else {
public init(rawValue: Int32) {
switch rawValue {
case 500:
self = .x0_5
case 1000:
self = .x1 self = .x1
case 1500:
self = .x1_5
case 2000:
self = .x2
case 4000:
self = .x4
case 8000:
self = .x8
case 16000:
self = .x16
default:
self = .custom(rawValue)
} }
} }
public var stringValue: String {
var stringValue = String(format: "%.1fx", self.doubleValue)
if stringValue.hasSuffix(".0x") {
stringValue = stringValue.replacingOccurrences(of: ".0x", with: "x")
}
return stringValue
}
} }
public struct MusicPlaybackSettings: Codable, Equatable { public struct MusicPlaybackSettings: Codable, Equatable {
@ -57,7 +104,7 @@ public struct MusicPlaybackSettings: Codable, Equatable {
self.order = MusicPlaybackSettingsOrder(rawValue: try container.decode(Int32.self, forKey: "order")) ?? .regular self.order = MusicPlaybackSettingsOrder(rawValue: try container.decode(Int32.self, forKey: "order")) ?? .regular
self.looping = MusicPlaybackSettingsLooping(rawValue: try container.decode(Int32.self, forKey: "looping")) ?? .none self.looping = MusicPlaybackSettingsLooping(rawValue: try container.decode(Int32.self, forKey: "looping")) ?? .none
self.voicePlaybackRate = AudioPlaybackRate(rawValue: try container.decodeIfPresent(Int32.self, forKey: "voicePlaybackRate") ?? AudioPlaybackRate.x1.rawValue) ?? .x1 self.voicePlaybackRate = AudioPlaybackRate(rawValue: try container.decodeIfPresent(Int32.self, forKey: "voicePlaybackRate") ?? AudioPlaybackRate.x1.rawValue)
} }
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {

View File

@ -26,7 +26,7 @@ public enum UndoOverlayContent {
case linkCopied(text: String) case linkCopied(text: String)
case banned(text: String) case banned(text: String)
case importedMessage(text: String) case importedMessage(text: String)
case audioRate(slowdown: Bool, text: String) case audioRate(rate: CGFloat, text: String)
case forward(savedMessages: Bool, text: String) case forward(savedMessages: Bool, text: String)
case autoDelete(isOn: Bool, title: String?, text: String) case autoDelete(isOn: Bool, title: String?, text: String)
case gigagroupConversion(text: String) case gigagroupConversion(text: String)

View File

@ -540,11 +540,21 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
displayUndo = false displayUndo = false
} }
self.originalRemainingSeconds = duration self.originalRemainingSeconds = duration
case let .audioRate(slowdown, text): case let .audioRate(rate, text):
self.avatarNode = nil self.avatarNode = nil
self.iconNode = nil self.iconNode = nil
self.iconCheckNode = nil self.iconCheckNode = nil
self.animationNode = AnimationNode(animation: slowdown ? "anim_voicespeedstop" : "anim_voicespeed", colors: [:], scale: 0.066)
let animationName: String
if rate == 1.5 {
animationName = "anim_voice1_5x"
} else if rate == 2.0 {
animationName = "anim_voice2x"
} else {
animationName = "anim_voice1x"
}
self.animationNode = AnimationNode(animation: animationName, colors: [:], scale: 0.066)
self.animatedStickerNode = nil self.animatedStickerNode = nil
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)

View File

@ -199,6 +199,12 @@ public final class WebAppController: ViewController, AttachmentContainable {
private var placeholderNode: ShimmerEffectNode? private var placeholderNode: ShimmerEffectNode?
fileprivate let loadingProgressPromise = Promise<CGFloat?>(nil) fileprivate let loadingProgressPromise = Promise<CGFloat?>(nil)
fileprivate var mainButtonState: AttachmentMainButtonState? {
didSet {
self.mainButtonStatePromise.set(.single(self.mainButtonState))
}
}
fileprivate let mainButtonStatePromise = Promise<AttachmentMainButtonState?>(nil) fileprivate let mainButtonStatePromise = Promise<AttachmentMainButtonState?>(nil)
private let context: AccountContext private let context: AccountContext
@ -397,6 +403,9 @@ public final class WebAppController: ViewController, AttachmentContainable {
} }
@objc fileprivate func mainButtonPressed() { @objc fileprivate func mainButtonPressed() {
if let mainButtonState = self.mainButtonState, !mainButtonState.isVisible || !mainButtonState.isEnabled {
return
}
self.webView?.lastTouchTimestamp = CACurrentMediaTime() self.webView?.lastTouchTimestamp = CACurrentMediaTime()
self.webView?.sendEvent(name: "main_button_pressed", data: nil) self.webView?.sendEvent(name: "main_button_pressed", data: nil)
} }
@ -635,7 +644,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
let isLoading = json["is_progress_visible"] as? Bool let isLoading = json["is_progress_visible"] as? Bool
let isEnabled = json["is_active"] as? Bool let isEnabled = json["is_active"] as? Bool
let state = AttachmentMainButtonState(text: text, backgroundColor: backgroundColor, textColor: textColor, isVisible: isVisible, isLoading: isLoading ?? false, isEnabled: isEnabled ?? true) let state = AttachmentMainButtonState(text: text, backgroundColor: backgroundColor, textColor: textColor, isVisible: isVisible, isLoading: isLoading ?? false, isEnabled: isEnabled ?? true)
self.mainButtonStatePromise.set(.single(state)) self.mainButtonState = state
} }
} }
case "web_app_request_viewport": case "web_app_request_viewport":
@ -971,7 +980,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
if let id = id { if let id = id {
paramsString = "{button_id: \"\(id)\"}" paramsString = "{button_id: \"\(id)\"}"
} }
self.webView?.sendEvent(name: "popup_closed", data: paramsString) self.webView?.sendEvent(name: "popup_closed", data: paramsString ?? "{}")
} }
fileprivate func sendPhoneRequestedEvent(phone: String?) { fileprivate func sendPhoneRequestedEvent(phone: String?) {