Various improvements

This commit is contained in:
Ilya Laktyushin 2025-08-27 17:32:48 +04:00
parent bbef11084a
commit 4a5e7ebed4
15 changed files with 545 additions and 93 deletions

View File

@ -14932,3 +14932,44 @@ Sorry for the inconvenience.";
"Notification.ChangedThemeGift" = "%1$@ changed chat theme to %2$@";
"Notification.YouChangedThemeGift" = "You changed chat theme to %@";
"Conversation.Theme.GiftTransfer.Text" = "This gift is already your theme in the chat with **%@**. Remove it there and use it here instead?";
"Conversation.Theme.GiftTransfer.Proceed" = "Yes";
"PeerInfo.Tabs.SetMainTab" = "Set as Main Tab";
"PeerInfo.Tabs.SetMainTab.Succeed" = "Tab order changed.";
"MediaPlayer.SavedMusic.AddToProfile" = "Add to Profile";
"MediaPlayer.SavedMusic.RemoveFromProfile" = "This audio is visible on your profile. [Remove >]()";
"MediaPlayer.SavedMusic.AddedToProfile.View" = "View";
"MediaPlayer.SavedMusic.AddedToProfile" = "Audio added to your profile.";
"MediaPlayer.SavedMusic.RemovedFromProfile" = "Audio removed from your profile.";
"MediaPlayer.ContextMenu.SaveToFiles" = "Save to Files";
"MediaPlayer.ContextMenu.SaveTo" = "Save to...";
"MediaPlayer.ContextMenu.SaveTo.Profile" = "Profile";
"MediaPlayer.ContextMenu.SaveTo.SavedMessages" = "Saved Messages";
"MediaPlayer.ContextMenu.SaveTo.Files" = "Files";
"MediaPlayer.ContextMenu.SaveTo.Info" = "Save to Files";
"MediaPlayer.ContextMenu.ShowInChat" = "Show in Chat";
"MediaPlayer.ContextMenu.Forward" = "Forward";
"MediaPlayer.ContextMenu.Delete" = "Delete";
"MediaPlayer.ContextMenu.Remove" = "Remove";
"MediaPlayer.Playlist.ThisChat" = "AUDIO IN THIS CHAT";
"MediaPlayer.Playlist.SavedMusic" = "%@'S PLAYLIST";
"MediaPlayer.Playlist.SavedMusicYou" = "YOUR PLAYLIST";
"Notification.PremiumGift.Stars_1" = "%@ Star";
"Notification.PremiumGift.Stars_any" = "%@ Stars";
"Ton.ProceedsOverview" = "PROCEEDS OVERVIEW";
"Ton.AvailableBalance" = "Balance Available to Withdraw";
"Ton.LifetimeProceeds" = "Total Lifetime Proceeds";
"Ton.WithdrawViaFragment" = "Withdraw via Fragment";
"Ton.WithdrawViaFragment.Info" = "Collect your TON using Fragment. [Learn More >]()";
"Ton.WithdrawViaFragment.Info_URL" = "https://telegram.org/tos/bot-developers#6-2-2-tpa-balance";
"MESSAGE_GIFT_THEME" = "%1$@ changed theme to %2$@";

View File

@ -809,6 +809,27 @@ public enum StarGift: Equatable, Codable, PostboxCoding {
themePeerId: self.themePeerId
)
}
public func withThemePeerId(_ themePeerId: EnginePeer.Id?) -> UniqueGift {
return UniqueGift(
id: self.id,
giftId: self.giftId,
title: self.title,
number: self.number,
slug: self.slug,
owner: self.owner,
attributes: self.attributes,
availability: self.availability,
giftAddress: self.giftAddress,
resellAmounts: self.resellAmounts,
resellForTonOnly: self.resellForTonOnly,
releasedBy: self.releasedBy,
valueAmount: self.valueAmount,
valueCurrency: self.valueCurrency,
flags: self.flags,
themePeerId: themePeerId
)
}
}
public enum DecodingError: Error {

View File

@ -148,6 +148,20 @@ public enum ChatTheme: PostboxCoding, Codable, Equatable {
}
}
}
public func withThemePeerId(_ themePeerId: EnginePeer.Id?) -> ChatTheme {
switch self {
case .emoticon:
return self
case let .gift(gift, themeSettings):
switch gift {
case let .unique(uniqueGift):
return .gift(.unique(uniqueGift.withThemePeerId(themePeerId)), themeSettings)
case .generic:
return self
}
}
}
}
extension ChatTheme {
@ -235,6 +249,22 @@ func _internal_setChatTheme(account: Account, peerId: PeerId, chatTheme: ChatThe
return .complete()
}
return account.postbox.transaction { transaction -> Signal<Void, NoError> in
var chatTheme = chatTheme
if case let .gift(gift, _) = chatTheme, case let .unique(uniqueGift) = gift, let previousThemePeerId = uniqueGift.themePeerId {
transaction.updatePeerCachedData(peerIds: Set([previousThemePeerId]), update: { _, current in
if let current = current as? CachedUserData {
return current.withUpdatedChatTheme(nil)
} else if let current = current as? CachedGroupData {
return current.withUpdatedChatTheme(nil)
} else if let current = current as? CachedChannelData {
return current.withUpdatedChatTheme(nil)
} else {
return current
}
})
}
chatTheme = chatTheme?.withThemePeerId(peerId)
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { _, current in
if let current = current as? CachedUserData {
return current.withUpdatedChatTheme(chatTheme)
@ -246,6 +276,7 @@ func _internal_setChatTheme(account: Account, peerId: PeerId, chatTheme: ChatThe
return current
}
})
let inputTheme: Api.InputChatTheme
if let chatTheme {
inputTheme = chatTheme.apiChatTheme
@ -466,7 +497,7 @@ public final class UniqueGiftChatThemesContext {
private let cacheDisposable = MetaDisposable()
private var themes: [ChatTheme] = []
private var nextOffset: Int32?
private var nextOffset: Int32 = 0
private var dataState: UniqueGiftChatThemesContext.State.DataState = .ready(canLoadMore: true)
private let stateValue = Promise<State>()
@ -487,7 +518,7 @@ public final class UniqueGiftChatThemesContext {
public func reload() {
self.themes = []
self.nextOffset = nil
self.nextOffset = 0
self.dataState = .ready(canLoadMore: true)
self.loadMore(reload: true)
}
@ -521,7 +552,7 @@ public final class UniqueGiftChatThemesContext {
self.pushState()
}
let signal = network.request(Api.functions.account.getUniqueGiftChatThemes(offset: offset ?? 0, limit: 32, hash: 0))
let signal = network.request(Api.functions.account.getUniqueGiftChatThemes(offset: offset, limit: 32, hash: 0))
|> map(Optional.init)
|> `catch` { error in
return .single(nil)
@ -557,7 +588,9 @@ public final class UniqueGiftChatThemesContext {
} else {
self.themes.append(contentsOf: themes)
}
if let nextOffset {
self.nextOffset = nextOffset
}
self.dataState = .ready(canLoadMore: nextOffset != nil)
self.pushState()
}))

View File

@ -830,8 +830,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
} else {
let price: String
if currency == "XTR" {
//TODO:localize
price = "\(amount) Stars"
price = strings.Notification_PremiumGift_Stars(Int32(clamping: amount))
} else {
price = formatCurrencyAmount(amount, currency: currency)
}

View File

@ -485,6 +485,7 @@ swift_library(
"//submodules/TelegramUI/Components/Stars/BalanceNeededScreen",
"//submodules/TelegramUI/Components/FaceScanScreen",
"//submodules/TelegramUI/Components/MediaManager/PeerMessagesMediaPlaylist",
"//submodules/TelegramUI/Components/ChatThemeScreen",
"//submodules/ContactsHelper",
] + select({
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,

View File

@ -0,0 +1,44 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatThemeScreen",
module_name = "ChatThemeScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",
"//submodules/AccountContext",
"//submodules/ComponentFlow",
"//submodules/TelegramPresentationData",
"//submodules/TelegramUIPreferences",
"//submodules/PresentationDataUtils",
"//submodules/TelegramNotices",
"//submodules/AnimationUI",
"//submodules/MergeLists",
"//submodules/MediaResources",
"//submodules/StickerResources",
"//submodules/WallpaperResources",
"//submodules/TooltipUI",
"//submodules/SolidRoundedButtonNode",
"//submodules/AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode",
"//submodules/ShimmerEffect",
"//submodules/AttachmentUI",
"//submodules/AvatarNode",
"//submodules/Markdown",
"//submodules/AppBundle",
"//submodules/ActivityIndicator",
"//submodules/TelegramUI/Components/Gifts/GiftItemComponent",
],
visibility = [
"//visibility:public",
],
)

View File

@ -21,12 +21,14 @@ import AnimatedStickerNode
import TelegramAnimatedStickerNode
import ShimmerEffect
import AttachmentUI
import AvatarNode
private struct ThemeSettingsThemeEntry: Comparable, Identifiable {
let index: Int
let chatTheme: ChatTheme?
let emojiFile: TelegramMediaFile?
let themeReference: PresentationThemeReference?
let peer: EnginePeer?
let nightMode: Bool
var selected: Bool
let theme: PresentationTheme
@ -47,6 +49,9 @@ private struct ThemeSettingsThemeEntry: Comparable, Identifiable {
if lhs.themeReference?.index != rhs.themeReference?.index {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.nightMode != rhs.nightMode {
return false
}
@ -70,16 +75,16 @@ private struct ThemeSettingsThemeEntry: Comparable, Identifiable {
}
func item(context: AccountContext, action: @escaping (ChatTheme?) -> Void) -> ListViewItem {
return ThemeSettingsThemeIconItem(context: context, chatTheme: self.chatTheme, emojiFile: self.emojiFile, themeReference: self.themeReference, nightMode: self.nightMode, selected: self.selected, theme: self.theme, strings: self.strings, wallpaper: self.wallpaper, action: action)
return ThemeSettingsThemeIconItem(context: context, chatTheme: self.chatTheme, emojiFile: self.emojiFile, themeReference: self.themeReference, peer: self.peer, nightMode: self.nightMode, selected: self.selected, theme: self.theme, strings: self.strings, wallpaper: self.wallpaper, action: action)
}
}
private class ThemeSettingsThemeIconItem: ListViewItem {
let context: AccountContext
let chatTheme: ChatTheme?
let emojiFile: TelegramMediaFile?
let themeReference: PresentationThemeReference?
let peer: EnginePeer?
let nightMode: Bool
let selected: Bool
let theme: PresentationTheme
@ -87,11 +92,24 @@ private class ThemeSettingsThemeIconItem: ListViewItem {
let wallpaper: TelegramWallpaper?
let action: (ChatTheme?) -> Void
public init(context: AccountContext, chatTheme: ChatTheme?, emojiFile: TelegramMediaFile?, themeReference: PresentationThemeReference?, nightMode: Bool, selected: Bool, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper?, action: @escaping (ChatTheme?) -> Void) {
public init(
context: AccountContext,
chatTheme: ChatTheme?,
emojiFile: TelegramMediaFile?,
themeReference: PresentationThemeReference?,
peer: EnginePeer?,
nightMode: Bool,
selected: Bool,
theme: PresentationTheme,
strings: PresentationStrings,
wallpaper: TelegramWallpaper?,
action: @escaping (ChatTheme?) -> Void
) {
self.context = context
self.chatTheme = chatTheme
self.emojiFile = emojiFile
self.themeReference = themeReference
self.peer = peer
self.nightMode = nightMode
self.selected = selected
self.theme = theme
@ -525,9 +543,9 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode {
}
}
final class ChatThemeScreen: ViewController {
static let themeCrossfadeDuration: Double = 0.3
static let themeCrossfadeDelay: Double = 0.25
public final class ChatThemeScreen: ViewController {
public static let themeCrossfadeDuration: Double = 0.3
public static let themeCrossfadeDelay: Double = 0.25
private var controllerNode: ChatThemeScreenNode {
return self.displayNode as! ChatThemeScreenNode
@ -539,7 +557,7 @@ final class ChatThemeScreen: ViewController {
private let animatedEmojiStickers: [String: [StickerPackItem]]
private let initiallySelectedTheme: ChatTheme?
private let peerName: String
let canResetWallpaper: Bool
fileprivate let canResetWallpaper: Bool
private let previewTheme: (ChatTheme?, Bool?) -> Void
fileprivate let changeWallpaper: () -> Void
fileprivate let resetWallpaper: () -> Void
@ -548,9 +566,9 @@ final class ChatThemeScreen: ViewController {
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
var dismissed: (() -> Void)?
public var dismissed: (() -> Void)?
var passthroughHitTestImpl: ((CGPoint) -> UIView?)? {
public var passthroughHitTestImpl: ((CGPoint) -> UIView?)? {
didSet {
if self.isNodeLoaded {
self.controllerNode.passthroughHitTestImpl = self.passthroughHitTestImpl
@ -558,7 +576,7 @@ final class ChatThemeScreen: ViewController {
}
}
init(
public init(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>),
animatedEmojiStickers: [String: [StickerPackItem]],
@ -657,7 +675,7 @@ final class ChatThemeScreen: ViewController {
}
}
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
override public func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
self.forEachController({ controller in
if let controller = controller as? TooltipScreen {
controller.dismiss()
@ -683,7 +701,7 @@ final class ChatThemeScreen: ViewController {
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
func dimTapped() {
public func dimTapped() {
self.controllerNode.dimTapped()
}
}
@ -908,28 +926,13 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega
chatTheme: nil,
emojiFile: nil,
themeReference: nil,
peer: nil,
nightMode: false,
selected: selectedTheme == nil,
theme: presentationData.theme,
strings: presentationData.strings,
wallpaper: nil
))
for theme in themes {
guard let emoticon = theme.emoticon else {
continue
}
entries.append(ThemeSettingsThemeEntry(
index: entries.count,
chatTheme: .emoticon(emoticon),
emojiFile: animatedEmojiStickers[emoticon]?.first?.file._parse(),
themeReference: .cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: nil, creatorAccountId: nil)),
nightMode: isDarkAppearance,
selected: selectedTheme?.id == ChatTheme.emoticon(emoticon).id,
theme: presentationData.theme,
strings: presentationData.strings,
wallpaper: nil
))
}
for theme in uniqueGiftChatThemesState.themes {
guard case let .gift(gift, themeSettings) = theme else {
continue
@ -954,6 +957,7 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega
chatTheme: theme,
emojiFile: emojiFile,
themeReference: .builtin(.dayClassic),
peer: nil,
nightMode: isDarkAppearance,
selected: selectedTheme?.id == theme.id,
theme: presentationData.theme,
@ -961,6 +965,24 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, ASScrollViewDelega
wallpaper: wallpaper
))
}
for theme in themes {
guard let emoticon = theme.emoticon else {
continue
}
entries.append(ThemeSettingsThemeEntry(
index: entries.count,
chatTheme: .emoticon(emoticon),
emojiFile: animatedEmojiStickers[emoticon]?.first?.file._parse(),
themeReference: .cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: nil, creatorAccountId: nil)),
peer: nil,
nightMode: isDarkAppearance,
selected: selectedTheme?.id == ChatTheme.emoticon(emoticon).id,
theme: presentationData.theme,
strings: presentationData.strings,
wallpaper: nil
))
}
let action: (ChatTheme?) -> Void = { [weak self] chatTheme in
if let self, self.selectedTheme != chatTheme {

View File

@ -0,0 +1,296 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ComponentFlow
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import AppBundle
import AvatarNode
import Markdown
import GiftItemComponent
import ActivityIndicator
private final class GiftThemeTransferAlertContentNode: AlertContentNode {
private let context: AccountContext
private let strings: PresentationStrings
private var presentationTheme: PresentationTheme
private let title: String
private let text: String
private let gift: StarGift.UniqueGift
private let titleNode: ASTextNode
private let giftView = ComponentView<Empty>()
private let textNode: ASTextNode
private let arrowNode: ASImageNode
private let avatarNode: AvatarNode
private let actionNodesSeparator: ASDisplayNode
private let actionNodes: [TextAlertContentActionNode]
private let actionVerticalSeparators: [ASDisplayNode]
private var activityIndicator: ActivityIndicator?
private var validLayout: CGSize?
var inProgress = false {
didSet {
if let size = self.validLayout {
let _ = self.updateLayout(size: size, transition: .immediate)
}
}
}
override var dismissOnOutsideTap: Bool {
return self.isUserInteractionEnabled
}
init(
context: AccountContext,
theme: AlertControllerTheme,
ptheme: PresentationTheme,
strings: PresentationStrings,
gift: StarGift.UniqueGift,
peer: EnginePeer,
title: String,
text: String,
actions: [TextAlertAction]
) {
self.context = context
self.strings = strings
self.presentationTheme = ptheme
self.title = title
self.text = text
self.gift = gift
self.titleNode = ASTextNode()
self.titleNode.maximumNumberOfLines = 0
self.textNode = ASTextNode()
self.textNode.maximumNumberOfLines = 0
self.arrowNode = ASImageNode()
self.arrowNode.displaysAsynchronously = false
self.arrowNode.displayWithoutProcessing = true
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
self.actionNodesSeparator = ASDisplayNode()
self.actionNodesSeparator.isLayerBacked = true
self.actionNodes = actions.map { action -> TextAlertContentActionNode in
return TextAlertContentActionNode(theme: theme, action: action)
}
var actionVerticalSeparators: [ASDisplayNode] = []
if actions.count > 1 {
for _ in 0 ..< actions.count - 1 {
let separatorNode = ASDisplayNode()
separatorNode.isLayerBacked = true
actionVerticalSeparators.append(separatorNode)
}
}
self.actionVerticalSeparators = actionVerticalSeparators
super.init()
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.arrowNode)
self.addSubnode(self.avatarNode)
self.addSubnode(self.actionNodesSeparator)
for actionNode in self.actionNodes {
self.addSubnode(actionNode)
}
for separatorNode in self.actionVerticalSeparators {
self.addSubnode(separatorNode)
}
self.updateTheme(theme)
self.avatarNode.setPeer(context: context, theme: ptheme, peer: peer)
}
override func updateTheme(_ theme: AlertControllerTheme) {
self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor)
self.textNode.attributedText = parseMarkdownIntoAttributedString(self.text, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor),
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor),
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor),
linkAttribute: { url in
return ("URL", url)
}
), textAlignment: .center)
self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/AlertArrow"), color: theme.secondaryColor)
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
actionNode.updateTheme(theme)
}
for separatorNode in self.actionVerticalSeparators {
separatorNode.backgroundColor = theme.separatorColor
}
if let size = self.validLayout {
_ = self.updateLayout(size: size, transition: .immediate)
}
}
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
var size = size
size.width = min(size.width, 270.0)
self.validLayout = size
var origin: CGPoint = CGPoint(x: 0.0, y: 20.0)
let avatarSize = CGSize(width: 60.0, height: 60.0)
self.avatarNode.updateSize(size: avatarSize)
let giftFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) - 44.0, y: origin.y), size: avatarSize)
let _ = self.giftView.update(
transition: .immediate,
component: AnyComponent(
GiftItemComponent(
context: self.context,
theme: self.presentationTheme,
strings: self.strings,
peer: nil,
subject: .uniqueGift(gift: self.gift, price: nil),
mode: .thumbnail
)
),
environment: {},
containerSize: avatarSize
)
if let view = self.giftView.view {
if view.superview == nil {
self.view.addSubview(view)
}
view.frame = giftFrame
}
if let arrowImage = self.arrowNode.image {
let arrowFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - arrowImage.size.width) / 2.0), y: origin.y + floorToScreenPixels((avatarSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size)
transition.updateFrame(node: self.arrowNode, frame: arrowFrame)
}
let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) + 44.0, y: origin.y), size: avatarSize)
transition.updateFrame(node: self.avatarNode, frame: avatarFrame)
origin.y += avatarSize.height + 17.0
let titleSize = self.titleNode.measure(CGSize(width: size.width - 32.0, height: size.height))
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize))
origin.y += titleSize.height + 5.0
let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize))
origin.y += textSize.height + 10.0
let actionButtonHeight: CGFloat = 44.0
var minActionsWidth: CGFloat = 0.0
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
let actionTitleInsets: CGFloat = 8.0
for actionNode in self.actionNodes {
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight))
minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets)
}
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
let contentWidth = max(size.width, minActionsWidth)
let actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count)
let resultSize = CGSize(width: contentWidth, height: avatarSize.height + titleSize.height + textSize.height + actionsHeight + 24.0 + insets.top + insets.bottom)
transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
var actionOffset: CGFloat = 0.0
var separatorIndex = -1
var nodeIndex = 0
for actionNode in self.actionNodes {
if separatorIndex >= 0 {
let separatorNode = self.actionVerticalSeparators[separatorIndex]
do {
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel)))
}
}
separatorIndex += 1
let currentActionWidth: CGFloat
do {
currentActionWidth = resultSize.width
}
let actionNodeFrame: CGRect
do {
actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight))
actionOffset += actionButtonHeight
}
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
nodeIndex += 1
}
if self.inProgress {
let activityIndicator: ActivityIndicator
if let current = self.activityIndicator {
activityIndicator = current
} else {
activityIndicator = ActivityIndicator(type: .custom(self.presentationTheme.list.freeInputField.controlColor, 18.0, 1.5, false))
self.addSubnode(activityIndicator)
}
if let actionNode = self.actionNodes.first {
actionNode.isHidden = true
let indicatorSize = CGSize(width: 22.0, height: 22.0)
transition.updateFrame(node: activityIndicator, frame: CGRect(origin: CGPoint(x: actionNode.frame.minX + floor((actionNode.frame.width - indicatorSize.width) / 2.0), y: actionNode.frame.minY + floor((actionNode.frame.height - indicatorSize.height) / 2.0)), size: indicatorSize))
}
}
return resultSize
}
}
public func giftThemeTransferAlertController(
context: AccountContext,
gift: StarGift.UniqueGift,
previousPeer: EnginePeer,
commit: @escaping () -> Void
) -> AlertController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let strings = presentationData.strings
var contentNode: GiftThemeTransferAlertContentNode?
var dismissImpl: ((Bool) -> Void)?
let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: presentationData.strings.Conversation_Theme_GiftTransfer_Proceed, action: { [weak contentNode] in
contentNode?.inProgress = true
commit()
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?(true)
})]
let text = strings.Conversation_Theme_GiftTransfer_Text(previousPeer.compactDisplayTitle).string
contentNode = GiftThemeTransferAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, gift: gift, peer: previousPeer, title: "", text: text, actions: actions)
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!)
dismissImpl = { [weak controller] animated in
if animated {
controller?.dismissAnimated()
} else {
controller?.dismiss()
}
}
return controller
}

View File

@ -1163,9 +1163,8 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat
return
}
//TODO:localize
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: "Set as Main Tab", icon: { theme in
items.append(.action(ContextMenuActionItem(text: params.presentationData.strings.PeerInfo_Tabs_SetMainTab, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.actionSheet.primaryTextColor)
}, action: { [weak self] _, f in
guard let self else {
@ -1183,7 +1182,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat
guard let self else {
return
}
let controller = UndoOverlayController(presentationData: params.presentationData, content: .actionSucceeded(title: nil, text: "Tab order changed.", cancel: nil, destructive: false), action: { _ in return true })
let controller = UndoOverlayController(presentationData: params.presentationData, content: .actionSucceeded(title: nil, text: params.presentationData.strings.PeerInfo_Tabs_SetMainTab_Succeed, cancel: nil, destructive: false), action: { _ in return true })
self.parentController?.present(controller, in: .current)
})
}

View File

@ -680,7 +680,7 @@ final class StarsTransactionsScreenComponent: Component {
theme: environment.theme,
header: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Proceeds Overview".uppercased(),
string: environment.strings.Ton_ProceedsOverview.uppercased(),
font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize),
textColor: environment.theme.list.freeTextColor
)),
@ -691,14 +691,14 @@ final class StarsTransactionsScreenComponent: Component {
AnyComponentWithIdentity(id: 0, component: AnyComponent(StarsOverviewItemComponent(
theme: environment.theme,
dateTimeFormat: environment.dateTimeFormat,
title: "Balance Available to Withdraw",
title: environment.strings.Ton_AvailableBalance,
value: self.revenueState?.balances.availableBalance ?? CurrencyAmount(amount: .zero, currency: .ton),
rate: self.revenueState?.usdRate ?? 0.0
))),
AnyComponentWithIdentity(id: 1, component: AnyComponent(StarsOverviewItemComponent(
theme: environment.theme,
dateTimeFormat: environment.dateTimeFormat,
title: "Total Lifetime Proceeds",
title: environment.strings.Ton_LifetimeProceeds,
value: self.revenueState?.balances.overallRevenue ?? CurrencyAmount(amount: .zero, currency: .ton),
rate: self.revenueState?.usdRate ?? 0.0
)))
@ -725,7 +725,7 @@ final class StarsTransactionsScreenComponent: Component {
return (TelegramTextAttributes.URL, contents)
})
let balanceInfoRawString = "Collect your TON using Fragment. [Learn More >]()"
let balanceInfoRawString = environment.strings.Ton_WithdrawViaFragment_Info
let balanceInfoString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(balanceInfoRawString, attributes: termsMarkdownAttributes, textAlignment: .natural))
if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== environment.theme {
self.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: environment.theme.list.itemAccentColor)!, environment.theme)
@ -754,7 +754,7 @@ final class StarsTransactionsScreenComponent: Component {
},
tapAction: { [weak self] attributes, _ in
if let controller = self?.controller?() as? StarsTransactionsScreen, let navigationController = controller.navigationController as? NavigationController {
component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: environment.strings.Stars_BotRevenue_Withdraw_Info_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: component.starsContext.ton ? environment.strings.Ton_WithdrawViaFragment_Info_URL : environment.strings.Stars_BotRevenue_Withdraw_Info_URL, forceExternal: false, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
}
}
)) : nil,
@ -766,7 +766,7 @@ final class StarsTransactionsScreenComponent: Component {
count: self.starsState?.balance ?? StarsAmount.zero,
currency: component.starsContext.ton ? .ton : .stars,
rate: nil,
actionTitle: component.starsContext.ton ? "Withdraw via Fragment" : (withdrawAvailable ? environment.strings.Stars_Intro_BuyShort : environment.strings.Stars_Intro_Buy),
actionTitle: component.starsContext.ton ? environment.strings.Ton_WithdrawViaFragment : (withdrawAvailable ? environment.strings.Stars_Intro_BuyShort : environment.strings.Stars_Intro_Buy),
actionAvailable: (!premiumConfiguration.areStarsDisabled && !premiumConfiguration.isPremiumDisabled),
actionIsEnabled: true,
actionIcon: component.starsContext.ton ? nil : PresentationResourcesItemList.itemListRoundTopupIcon(environment.theme),

View File

@ -123,6 +123,7 @@ import ChatEmptyNode
import ChatMediaInputStickerGridItem
import AdsInfoScreen
import Photos
import ChatThemeScreen
extension ChatControllerImpl {
public func presentThemeSelection() {
@ -186,11 +187,11 @@ extension ChatControllerImpl {
}
},
changeWallpaper: { [weak self] in
guard let strongSelf = self, let peerId else {
guard let self, let peerId else {
return
}
if let themeController = strongSelf.themeScreen {
strongSelf.themeScreen = nil
if let themeController = self.themeScreen {
self.themeScreen = nil
themeController.dimTapped()
}
let dismissControllers = { [weak self] in
@ -206,60 +207,58 @@ extension ChatControllerImpl {
}
var openWallpaperPickerImpl: ((Bool) -> Void)?
let openWallpaperPicker = { [weak self] animateAppearance in
guard let strongSelf = self else {
guard let self else {
return
}
let controller = wallpaperMediaPickerController(
context: strongSelf.context,
updatedPresentationData: strongSelf.updatedPresentationData,
context: context,
updatedPresentationData: self.updatedPresentationData,
peer: EnginePeer(peer),
animateAppearance: animateAppearance,
completion: { [weak self] _, result in
guard let strongSelf = self, let asset = result as? PHAsset else {
guard let self, let asset = result as? PHAsset else {
return
}
let controller = WallpaperGalleryController(context: strongSelf.context, source: .asset(asset), mode: .peer(EnginePeer(peer), false))
let controller = WallpaperGalleryController(context: context, source: .asset(asset), mode: .peer(EnginePeer(peer), false))
controller.navigationPresentation = .modal
controller.apply = { [weak self] wallpaper, options, editedImage, cropRect, brightness, forBoth in
if let strongSelf = self {
uploadCustomPeerWallpaper(context: strongSelf.context, wallpaper: wallpaper, mode: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness, peerId: peerId, forBoth: forBoth, completion: {
Queue.mainQueue().after(0.3, {
dismissControllers()
})
controller.apply = { wallpaper, options, editedImage, cropRect, brightness, forBoth in
uploadCustomPeerWallpaper(context: context, wallpaper: wallpaper, mode: options, editedImage: editedImage, cropRect: cropRect, brightness: brightness, peerId: peerId, forBoth: forBoth, completion: {
Queue.mainQueue().after(0.3, {
dismissControllers()
})
}
})
}
strongSelf.push(controller)
self.push(controller)
},
openColors: { [weak self] in
guard let strongSelf = self else {
guard let self else {
return
}
let controller = standaloneColorPickerController(context: strongSelf.context, peer: EnginePeer(peer), push: { [weak self] controller in
if let strongSelf = self {
strongSelf.push(controller)
let controller = standaloneColorPickerController(context: context, peer: EnginePeer(peer), push: { [weak self] controller in
if let self {
self.push(controller)
}
}, openGallery: {
openWallpaperPickerImpl?(false)
})
controller.navigationPresentation = .flatModal
strongSelf.push(controller)
self.push(controller)
}
)
controller.navigationPresentation = .flatModal
strongSelf.push(controller)
self.push(controller)
}
openWallpaperPickerImpl = openWallpaperPicker
openWallpaperPicker(true)
},
resetWallpaper: { [weak self] in
guard let strongSelf = self, let peerId else {
guard let self, let peerId else {
return
}
let _ = strongSelf.context.engine.themes.setChatWallpaper(peerId: peerId, wallpaper: nil, forBoth: false).startStandalone()
let _ = self.context.engine.themes.setChatWallpaper(peerId: peerId, wallpaper: nil, forBoth: false).startStandalone()
},
completion: { [weak self] chatTheme in
guard let strongSelf = self, let peerId else {
guard let self, let peerId else {
return
}
if canResetWallpaper && chatTheme != nil {
@ -267,8 +266,8 @@ extension ChatControllerImpl {
}
strongSelf.chatThemeAndDarkAppearancePreviewPromise.set(.single((chatTheme ?? .emoticon(""), nil)))
let _ = context.engine.themes.setChatTheme(peerId: peerId, chatTheme: chatTheme ?? .emoticon("")).startStandalone(completed: { [weak self] in
if let strongSelf = self {
strongSelf.chatThemeAndDarkAppearancePreviewPromise.set(.single((nil, nil)))
if let self {
self.chatThemeAndDarkAppearancePreviewPromise.set(.single((nil, nil)))
}
})
}

View File

@ -141,6 +141,7 @@ import SuggestedPostApproveAlert
import AVFoundation
import BalanceNeededScreen
import FaceScanScreen
import ChatThemeScreen
public final class ChatControllerOverlayPresentationData {
public let expandData: (ASDisplayNode?, () -> Void)

View File

@ -46,6 +46,7 @@ import ComponentFlow
import ChatEmptyNode
import SpaceWarpView
import ChatSideTopicsPanel
import ChatThemeScreen
final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem {
let itemNode: OverlayMediaItemNode

View File

@ -570,19 +570,18 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
func addToSavedMusic(file: FileMediaReference) {
self.dismissAllTooltips()
var actionText: String? = "View"
var actionText: String? = self.presentationData.strings.MediaPlayer_SavedMusic_AddedToProfile_View
if let itemId = self.controlsNode.currentItemId as? PeerMessagesMediaPlaylistItemId, itemId.messageId.namespace == Namespaces.Message.Local && itemId.messageId.peerId == self.context.account.peerId {
actionText = nil
}
//TODO:localize
let controller = UndoOverlayController(
presentationData: self.presentationData,
content: .universalImage(
image: generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SavedMusic"), color: .white)!,
size: nil,
title: nil,
text: "Audio added to your profile.",
text: self.presentationData.strings.MediaPlayer_SavedMusic_AddedToProfile,
customUndoText: actionText,
timeout: 3.0
),
@ -620,14 +619,13 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
func removeFromSavedMusic(file: FileMediaReference) {
self.dismissAllTooltips()
//TODO:localize
let controller = UndoOverlayController(
presentationData: self.presentationData,
content: .universalImage(
image: generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SavedMusic"), color: .white)!,
size: nil,
title: nil,
text: "Audio removed from your profile.",
text: self.presentationData.strings.MediaPlayer_SavedMusic_RemovedFromProfile,
customUndoText: nil,
timeout: 3.0
),
@ -1029,10 +1027,9 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
}
var items: [ContextMenuItem] = []
//TODO:localize
if canSaveToProfile || canSaveToSavedMessages {
items.append(
.action(ContextMenuActionItem(text: "Save to...", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/DownloadTone"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in
.action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveTo, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/DownloadTone"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in
if let self {
var subActions: [ContextMenuItem] = []
subActions.append(
@ -1044,7 +1041,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
if canSaveToProfile {
subActions.append(
.action(ContextMenuActionItem(text: "Profile", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
.action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveTo_Profile, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/User"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.default)
if let self {
@ -1056,7 +1053,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
if canSaveToSavedMessages {
subActions.append(
.action(ContextMenuActionItem(text: "Saved Messages", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
.action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveTo_SavedMessages, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.default)
if let self {
@ -1067,7 +1064,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
}
subActions.append(
.action(ContextMenuActionItem(text: "Files", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
.action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveTo_Files, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.default)
if let self {
@ -1091,7 +1088,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
let noAction: ((ContextMenuActionItem.Action) -> Void)? = nil
subActions.append(
.action(ContextMenuActionItem(text: "Choose where you want this audio to be saved.", textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: noAction))
.action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveTo_Info, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: noAction))
)
c?.pushItems(items: .single(ContextController.Items(content: .list(subActions))))
@ -1099,7 +1096,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
}))
)
} else {
items.append(.action(ContextMenuActionItem(text: "Save to Files", icon: { theme in
items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_SaveToFiles, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.actionSheet.primaryTextColor)
}, action: { [weak self] _, f in
f(.default)
@ -1131,7 +1128,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
addedSeparator = true
}
items.append(
.action(ContextMenuActionItem(text: "Show in Chat", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
.action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_ShowInChat, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
f(.dismissWithoutContent)
guard let self else {
@ -1144,7 +1141,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
}
// items.append(
// .action(ContextMenuActionItem(text: "Forward", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
// .action(ContextMenuActionItem(text: presentationData.strings.MediaPlayer_ContextMenu_Forward, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
// f(.default)
//
// if let _ = self {
@ -1181,9 +1178,9 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, ASGestu
items.append(.separator)
addedSeparator = true
}
var actionTitle = "Delete"
var actionTitle = presentationData.strings.MediaPlayer_ContextMenu_Delete
if case .custom = self.source {
actionTitle = "Remove"
actionTitle = presentationData.strings.MediaPlayer_ContextMenu_Remove
}
items.append(
.action(ContextMenuActionItem(text: actionTitle, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] c, f in

View File

@ -1026,13 +1026,12 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
self.separatorNode.isHidden = hasSectionHeader
if hasSectionHeader {
//TODO:localize
let sideInset: CGFloat = 16.0
var sectionTitle = "AUDIO IN THIS CHAT"
var sectionTitle = self.presentationData.strings.MediaPlayer_Playlist_ThisChat
if let peerName = self.peerName {
sectionTitle = "\(peerName.uppercased())'S PLAYLIST"
sectionTitle = self.presentationData.strings.MediaPlayer_Playlist_SavedMusic(peerName.uppercased()).string
} else if case .custom = self.source {
sectionTitle = "YOUR PLAYLIST"
sectionTitle = self.presentationData.strings.MediaPlayer_Playlist_SavedMusicYou
}
let sectionTitleSize = self.sectionTitle.update(
transition: .immediate,
@ -1096,7 +1095,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
return (TelegramTextAttributes.URL, contents)
})
let attributedString = parseMarkdownIntoAttributedString("This audio is visible on your profile. [Remove >]()", attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString
let attributedString = parseMarkdownIntoAttributedString(self.presentationData.strings.MediaPlayer_SavedMusic_RemoveFromProfile, attributes: markdownAttributes, textAlignment: .center).mutableCopy() as! NSMutableAttributedString
if let range = attributedString.string.range(of: ">"), let chevronImage = self.cachedChevronImage?.0 {
attributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedString.string))
attributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: attributedString.string))
@ -1125,7 +1124,6 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
))
profileAudioOffset = 18.0
} else {
//TODO:localize
profileAudioComponent = AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
color: self.presentationData.theme.list.itemCheckColors.fillColor,
@ -1139,7 +1137,7 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
BundleIconComponent(name: "Peer Info/SaveMusic", tintColor: self.presentationData.theme.list.itemCheckColors.foregroundColor)
)),
AnyComponentWithIdentity(id: "label", component: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: "Add to Profile", font: Font.semibold(17.0), textColor: self.presentationData.theme.list.itemCheckColors.foregroundColor)))
MultilineTextComponent(text: .plain(NSAttributedString(string: self.presentationData.strings.MediaPlayer_SavedMusic_AddToProfile, font: Font.semibold(17.0), textColor: self.presentationData.theme.list.itemCheckColors.foregroundColor)))
))
], spacing: 8.0)
)),