feat: новые функции, исправлены критические ошибки сборки и баги интерфейса, больше подписей в файлах

This commit is contained in:
ichmagmaus 812
2026-03-04 22:06:16 +01:00
parent a614259289
commit f033954db2
81 changed files with 1256 additions and 298 deletions
+2
View File
@@ -84,3 +84,5 @@ xcode-files
/codesigning/
/build-system/real-codesigning/
/build-system/local-codesigning/
build-output/
build-output/
+14 -1
View File
@@ -36,6 +36,7 @@ load(
"telegram_bazel_path",
"telegram_use_xcode_managed_codesigning",
"telegram_bundle_id",
"telegram_is_appstore_build",
"telegram_aps_environment",
"telegram_team_id",
"telegram_enable_icloud",
@@ -509,6 +510,16 @@ aps_fragment = "" if telegram_aps_environment == "" else """
<string>{telegram_aps_environment}</string>
""".format(telegram_aps_environment=telegram_aps_environment)
beta_reports_active_fragment = "" if telegram_is_appstore_build != "true" else """
<key>beta-reports-active</key>
<true/>
"""
get_task_allow_fragment = """
<key>get-task-allow</key>
<{value}/>
""".format(value = "false" if telegram_is_appstore_build == "true" else "true")
app_groups_fragment = """
<key>com.apple.security.application-groups</key>
<array>
@@ -546,6 +557,8 @@ plist_fragment(
extension = "entitlements",
template = "".join([
aps_fragment,
beta_reports_active_fragment,
get_task_allow_fragment,
app_groups_fragment,
siri_fragment,
associated_domains_fragment,
@@ -1718,7 +1731,7 @@ ios_application(
":RequiredDeviceCapabilitiesPlist",
":UrlTypesInfoPlist",
],
app_icons = [ ":{}_icon".format(name) for name in composer_icon_folders ],
app_icons = [":DefaultAppIcon"],
alternate_icons = [
":{}".format(name) for name in alternate_icon_folders
],
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 665 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

@@ -1,116 +1,92 @@
{
"images": [
"images" : [
{
"filename": "GhostIcon@40x40.png",
"idiom": "iphone",
"scale": "2x",
"size": "20x20"
"filename" : "GhostIcon@40x40.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"filename": "GhostIcon@60x60.png",
"idiom": "iphone",
"scale": "3x",
"size": "20x20"
"filename" : "GhostIcon@60x60.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"filename": "GhostIcon@58x58.png",
"idiom": "iphone",
"scale": "2x",
"size": "29x29"
"filename" : "GhostIcon@58x58.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"filename": "GhostIcon@87x87.png",
"idiom": "iphone",
"scale": "3x",
"size": "29x29"
"filename" : "GhostIcon@87x87.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"filename": "GhostIcon@80x80.png",
"idiom": "iphone",
"scale": "2x",
"size": "40x40"
"filename" : "GhostIcon@80x80.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"filename": "GhostIcon@120x120.png",
"idiom": "iphone",
"scale": "3x",
"size": "40x40"
"filename" : "GhostIcon@120x120.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"filename": "GhostIcon@120x120.png",
"idiom": "iphone",
"scale": "2x",
"size": "60x60"
"filename" : "GhostIcon@120x120.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"filename": "GhostIcon@180x180.png",
"idiom": "iphone",
"scale": "3x",
"size": "60x60"
"filename" : "GhostIcon@180x180.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"filename": "GhostIcon@20x20.png",
"idiom": "ipad",
"scale": "1x",
"size": "20x20"
"filename" : "GhostIcon@40x40.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"filename": "GhostIcon@40x40.png",
"idiom": "ipad",
"scale": "2x",
"size": "20x20"
"filename" : "GhostIcon@58x58.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"filename": "GhostIcon@29x29.png",
"idiom": "ipad",
"scale": "1x",
"size": "29x29"
"filename" : "GhostIcon@80x80.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"filename": "GhostIcon@58x58.png",
"idiom": "ipad",
"scale": "2x",
"size": "29x29"
"filename" : "GhostIcon@152x152.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"filename": "GhostIcon@40x40.png",
"idiom": "ipad",
"scale": "1x",
"size": "40x40"
"filename" : "GhostIcon@167x167.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"filename": "GhostIcon@80x80.png",
"idiom": "ipad",
"scale": "2x",
"size": "40x40"
},
{
"filename": "GhostIcon@76x76.png",
"idiom": "ipad",
"scale": "1x",
"size": "76x76"
},
{
"filename": "GhostIcon@152x152.png",
"idiom": "ipad",
"scale": "2x",
"size": "76x76"
},
{
"filename": "GhostIcon@167x167.png",
"idiom": "ipad",
"scale": "2x",
"size": "83.5x83.5"
},
{
"filename": "GhostIcon@1024x1024.png",
"idiom": "ios-marketing",
"scale": "1x",
"size": "1024x1024"
"filename" : "GhostIcon@1024x1024.png",
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info": {
"author": "xcode",
"version": 1
"info" : {
"author" : "xcode",
"version" : 1
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 623 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

@@ -241,7 +241,7 @@ def generate(header_path: str, implementation_path: str, data_path: str, entries
arguments_array += ', '
arguments_array += '[[NSString alloc] initWithFormat:@"%@", arg{}]'.format(i)
formatted_accessors += '''
static _FormattedString * _Nonnull getFormatted{num_arguments}(_PresentationStrings * _Nonnull strings,
static __attribute__((unused)) _FormattedString * _Nonnull getFormatted{num_arguments}(_PresentationStrings * _Nonnull strings,
uint32_t keyId{arguments_string}) {{
NSString *formatString = getSingle(strings, strings->_idToKey[@(keyId)], nil);
NSArray<_FormattedStringRange *> *argumentRanges = extractArgumentRanges(formatString);
+5
View File
@@ -232,6 +232,9 @@ class BazelCommandLine:
combined_arguments += self.common_debug_args
combined_arguments += self.get_define_arguments()
if self.disable_provisioning_profiles:
combined_arguments += ['--//Telegram:disableProvisioningProfiles']
if self.remote_cache is not None:
combined_arguments += [
'--remote_cache={}'.format(self.remote_cache),
@@ -559,6 +562,8 @@ def generate_project(bazel, arguments):
disable_extensions = arguments.disableExtensions
if arguments.disableProvisioningProfiles is not None:
disable_provisioning_profiles = arguments.disableProvisioningProfiles
if disable_provisioning_profiles:
bazel_command_line.set_disable_provisioning_profiles()
if arguments.projectIncludeRelease is not None:
project_include_release = arguments.projectIncludeRelease
if arguments.xcodeManagedCodesigning is not None and arguments.xcodeManagedCodesigning == True:
+4
View File
@@ -26,6 +26,8 @@ def generate_xcodeproj(build_environment: BuildEnvironment, disable_extensions,
if target_name == 'Telegram':
if disable_extensions:
bazel_generate_arguments += ['--//{}:disableExtensions'.format(app_target)]
if disable_provisioning_profiles:
bazel_generate_arguments += ['--//{}:disableProvisioningProfiles'.format(app_target)]
bazel_generate_arguments += ['--//{}:disableStripping'.format(app_target)]
project_bazel_arguments = []
@@ -35,6 +37,8 @@ def generate_xcodeproj(build_environment: BuildEnvironment, disable_extensions,
if target_name == 'Telegram':
if disable_extensions:
project_bazel_arguments += ['--//{}:disableExtensions'.format(app_target)]
if disable_provisioning_profiles:
project_bazel_arguments += ['--//{}:disableProvisioningProfiles'.format(app_target)]
project_bazel_arguments += ['--//{}:disableStripping'.format(app_target)]
project_bazel_arguments += ['--features=-swift.debug_prefix_map']
+1 -1
View File
@@ -4,7 +4,7 @@
"api_hash": "",
"team_id": "",
"app_center_id": "0",
"is_internal_build": "false",
"is_internal_build": "true",
"is_appstore_build": "true",
"appstore_id": "0",
"app_specific_url_scheme": "ghostgram",
+2
View File
@@ -16,9 +16,11 @@ objc_library(
], allow_empty=True) + private_headers,
copts = [
"-Werror",
"-Wno-deprecated-declarations",
],
cxxopts = [
"-Werror",
"-Wno-deprecated-declarations",
"-std=c++17",
],
hdrs = public_headers,
@@ -89,7 +89,7 @@ public class ChatListSearchItemNode: ListViewItemNode {
required public init() {
self.searchBarNode = SearchBarPlaceholderNode(fieldStyle: .modern)
super.init(layerBacked: false, dynamicBounce: false)
super.init(layerBacked: false)
self.addSubnode(self.searchBarNode)
}
@@ -107,7 +107,6 @@ public class ChatListSearchItemNode: ListViewItemNode {
}
public func asyncLayout() -> (_ item: ChatListSearchItem, _ params: ListViewItemLayoutParams, _ nextIsPinned: Bool, _ isEnabled: Bool) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let searchBarNodeLayout = self.searchBarNode.asyncLayout()
let placeholder = self.placeholder
return { [weak self] item, params, nextIsPinned, isEnabled in
@@ -115,9 +114,9 @@ public class ChatListSearchItemNode: ListViewItemNode {
let backgroundColor = nextIsPinned ? item.theme.chatList.pinnedItemBackgroundColor : item.theme.chatList.itemBackgroundColor
let placeholderColor = item.theme.list.itemSecondaryTextColor
let controlColor = item.theme.chat.inputPanel.panelControlColor
let placeholderString = NSAttributedString(string: placeholder ?? "", font: searchBarFont, textColor: placeholderColor)
let (_, searchBarApply) = searchBarNodeLayout(placeholderString, placeholderString, CGSize(width: baseWidth - 20.0, height: 36.0), 1.0, placeholderColor, nextIsPinned ? item.theme.chatList.pinnedSearchBarColor : item.theme.chatList.regularSearchBarColor, backgroundColor, .immediate)
let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 54.0), insets: UIEdgeInsets())
@@ -132,9 +131,7 @@ public class ChatListSearchItemNode: ListViewItemNode {
let searchBarFrame = CGRect(origin: CGPoint(x: params.leftInset + 10.0, y: 8.0), size: CGSize(width: baseWidth - 20.0, height: 36.0))
strongSelf.searchBarNode.frame = searchBarFrame
searchBarApply()
strongSelf.searchBarNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: baseWidth - 20.0, height: 36.0))
_ = strongSelf.searchBarNode.updateLayout(placeholderString: placeholderString, compactPlaceholderString: placeholderString, constrainedSize: searchBarFrame.size, expansionProgress: 1.0, iconColor: placeholderColor, foregroundColor: nextIsPinned ? item.theme.chatList.pinnedSearchBarColor : item.theme.chatList.regularSearchBarColor, backgroundColor: backgroundColor, controlColor: controlColor, transition: transition)
if !item.isEnabled {
if strongSelf.disabledOverlay == nil {
+8 -3
View File
@@ -3,9 +3,14 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ChatListUI",
module_name = "ChatListUI",
srcs = glob([
"Sources/**/*.swift",
]),
srcs = glob(
[
"Sources/**/*.swift",
],
exclude = [
"Sources/ChatListFilterTabContainerNode.swift",
],
),
copts = [
"-warnings-as-errors",
],
@@ -6595,8 +6595,17 @@ private final class ChatListLocationContext {
var proxyButton: AnyComponentWithIdentity<NavigationButtonComponentEnvironment>?
var storyButton: AnyComponentWithIdentity<NavigationButtonComponentEnvironment>?
// GHOSTGRAM: Account switcher liquid glass avatar button for the next account
var accountSwitcherButton: AnyComponentWithIdentity<NavigationButtonComponentEnvironment>?
private var accountSwitcherDisposable: Disposable?
private var accountSwitcherAvatarDisposable: Disposable?
var rightButtons: [AnyComponentWithIdentity<NavigationButtonComponentEnvironment>] {
var result: [AnyComponentWithIdentity<NavigationButtonComponentEnvironment>] = []
// Account switcher is first leftmost of the right-side buttons
if let accountSwitcherButton = self.accountSwitcherButton {
result.append(accountSwitcherButton)
}
if let rightButton = self.rightButton {
result.append(rightButton)
}
@@ -6631,6 +6640,90 @@ private final class ChatListLocationContext {
self.location = location
self.parentController = parentController
// GHOSTGRAM: Subscribe to account list and maintain the switcher button
if case .chatList(.root) = location {
self.accountSwitcherDisposable = (context.sharedContext.activeAccountsWithInfo
|> deliverOnMainQueue)
.start(next: { [weak self] (info: (primary: AccountRecordId?, accounts: [AccountWithInfo])) in
guard let self else { return }
let primaryId = info.primary
let accounts = info.accounts
// Only show when there is more than one account
guard accounts.count > 1, let primaryId = primaryId else {
if self.accountSwitcherButton != nil {
self.accountSwitcherButton = nil
let _ = self.parentController?.updateHeaderContent()
self.parentController?.requestLayout(transition: .immediate)
}
return
}
// Find next account cyclically
let currentIndex = accounts.firstIndex(where: { $0.account.id == primaryId }) ?? 0
let nextIndex = (currentIndex + 1) % accounts.count
let nextAccount = accounts[nextIndex]
let nextPeer = nextAccount.peer
let nextPeerId = "\(nextAccount.account.id)"
// Build button placeholder immediately (image loads async)
let buildButton: (UIImage?) -> Void = { [weak self] image in
guard let self else { return }
guard case .chatList(.root) = self.location else { return }
let sharedContext = self.context.sharedContext
let nextAccountId = nextAccount.account.id
self.accountSwitcherButton = AnyComponentWithIdentity(
id: "accountSwitcher",
component: AnyComponent(NavigationButtonComponent(
content: .avatar(peerId: nextPeerId, avatarImage: image),
pressed: { [weak sharedContext] _ in
sharedContext?.switchToAccount(id: nextAccountId, fromSettingsController: nil, withChatListController: nil)
}
))
)
// Trigger header rebuild
let _ = self.parentController?.updateHeaderContent()
self.parentController?.requestLayout(transition: .immediate)
}
// Attempt to load the peer's avatar from mediaBox
if let representation = nextPeer.smallProfileImage {
self.accountSwitcherAvatarDisposable?.dispose()
let resource = representation.resource
let account = nextAccount.account
// Try to read cached data first; if not ready, trigger a fetch then watch for completion
self.accountSwitcherAvatarDisposable = (account.postbox.mediaBox
.resourceData(resource)
|> deliverOnMainQueue)
.start(next: { data in
if data.complete, let uiImage = UIImage(contentsOfFile: data.path) {
buildButton(uiImage)
}
}, completed: {
// If resource was never complete after signal ended, show placeholder
buildButton(nil)
})
// Trigger the actual network fetch so mediaBox populates the resource
if let peerReference = PeerReference(nextPeer) {
let _ = fetchedMediaResource(
mediaBox: account.postbox.mediaBox,
userLocation: .peer(nextPeer.id),
userContentType: .avatar,
reference: .avatar(peer: peerReference, resource: resource)
).start()
}
} else {
// No photo show placeholder
buildButton(nil)
}
})
}
let hasProxy = context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.proxySettings])
|> map { sharedData -> (Bool, Bool) in
if let settings = sharedData.entries[SharedDataKeys.proxySettings]?.get(ProxySettings.self) {
@@ -6949,6 +7042,8 @@ private final class ChatListLocationContext {
deinit {
self.titleDisposable?.dispose()
self.stateDisposable?.dispose()
self.accountSwitcherDisposable?.dispose()
self.accountSwitcherAvatarDisposable?.dispose()
}
private func updateChatList(
@@ -6982,6 +7077,7 @@ private final class ChatListLocationContext {
if case .chatList(.root) = self.location {
self.rightButton = nil
self.storyButton = nil
self.accountSwitcherButton = nil
}
let title = !stateAndFilterId.state.selectedPeerIds.isEmpty ? presentationData.strings.ChatList_SelectedChats(Int32(stateAndFilterId.state.selectedPeerIds.count)) : defaultTitle
@@ -6997,6 +7093,7 @@ private final class ChatListLocationContext {
if case .chatList(.root) = self.location {
self.rightButton = nil
self.storyButton = nil
self.accountSwitcherButton = nil
}
self.leftButton = AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent(
content: .text(title: presentationData.strings.Common_Done, isBold: true),
@@ -14,6 +14,7 @@ import MergedAvatarsNode
import TextNodeWithEntities
import TextFormat
import AvatarNode
import GlobalControlPanelsContext
class ChatListNoticeItem: ListViewItem {
enum Action {
@@ -25,12 +26,12 @@ class ChatListNoticeItem: ListViewItem {
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let notice: ChatListNotice
let notice: GlobalControlPanelsContext.ChatListNotice
let action: (Action) -> Void
let selectable: Bool = true
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, notice: ChatListNotice, action: @escaping (Action) -> Void) {
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, notice: GlobalControlPanelsContext.ChatListNotice, action: @escaping (Action) -> Void) {
self.context = context
self.theme = theme
self.strings = strings
@@ -130,7 +131,7 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode {
self.arrowNode = ASImageNode()
self.separatorNode = ASDisplayNode()
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
super.init(layerBacked: false, rotated: false, seeThrough: false)
self.contentContainer.clipsToBounds = true
self.clipsToBounds = true
@@ -122,10 +122,6 @@ public func navigationBarBackArrowImage(color: UIColor) -> UIImage? {
}
}
public protocol NavigationButtonCustomDisplayNode {
var isHighlightable: Bool { get }
}
public protocol NavigationButtonNode: ASDisplayNode {
func updateManualAlpha(alpha: CGFloat, transition: ContainedViewLayoutTransition)
var mainContentNode: ASDisplayNode? { get }
@@ -328,7 +328,7 @@ private final class NavigationButtonItemNode: ImmediateTextNode {
}
public final class NavigationButtonNode: ContextControllerSourceNode {
public final class NavigationButtonNodeImpl: ContextControllerSourceNode, NavigationButtonNode {
private var nodes: [NavigationButtonItemNode] = []
private var disappearingNodes: [(frame: CGRect, size: CGSize, node: NavigationButtonItemNode)] = []
@@ -1,12 +1,22 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import Postbox
import TelegramPresentationData
import ItemListUI
import AccountContext
import ComponentFlow
import SliderComponent
private let minDeletedMessageTransparencyPercent: Int32 = Int32(AntiDeleteManager.minDeletedMessageTransparency * 100.0)
private let maxDeletedMessageTransparencyPercent: Int32 = Int32(AntiDeleteManager.maxDeletedMessageTransparency * 100.0)
private func clampDeletedMessageTransparencyPercent(_ value: Int32) -> Int32 {
return max(minDeletedMessageTransparencyPercent, min(maxDeletedMessageTransparencyPercent, value))
}
// MARK: - Entry Definition
@@ -17,6 +27,7 @@ private enum DeletedMessagesSection: Int32 {
private enum DeletedMessagesEntry: ItemListNodeEntry {
case enableToggle(PresentationTheme, String, Bool)
case archiveMediaToggle(PresentationTheme, String, Bool)
case transparencySlider(PresentationTheme, Int32, Bool)
case settingsInfo(PresentationTheme, String)
var section: ItemListSectionId {
@@ -29,8 +40,10 @@ private enum DeletedMessagesEntry: ItemListNodeEntry {
return 0
case .archiveMediaToggle:
return 1
case .settingsInfo:
case .transparencySlider:
return 2
case .settingsInfo:
return 3
}
}
@@ -48,6 +61,12 @@ private enum DeletedMessagesEntry: ItemListNodeEntry {
return true
}
return false
case let .transparencySlider(lhsTheme, lhsValue, lhsIsEnabled):
if case let .transparencySlider(rhsTheme, rhsValue, rhsIsEnabled) = rhs,
lhsTheme === rhsTheme, lhsValue == rhsValue, lhsIsEnabled == rhsIsEnabled {
return true
}
return false
case let .settingsInfo(lhsTheme, lhsText):
if case let .settingsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
@@ -85,6 +104,16 @@ private enum DeletedMessagesEntry: ItemListNodeEntry {
arguments.toggleArchiveMedia(value)
}
)
case let .transparencySlider(theme, value, isEnabled):
return DeletedMessagesTransparencySliderItem(
theme: theme,
value: value,
isEnabled: isEnabled,
sectionId: self.section,
updated: { value in
arguments.updateTransparency(value)
}
)
case let .settingsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
}
@@ -96,13 +125,16 @@ private enum DeletedMessagesEntry: ItemListNodeEntry {
private final class DeletedMessagesControllerArguments {
let toggleEnabled: (Bool) -> Void
let toggleArchiveMedia: (Bool) -> Void
let updateTransparency: (Int32) -> Void
init(
toggleEnabled: @escaping (Bool) -> Void,
toggleArchiveMedia: @escaping (Bool) -> Void
toggleArchiveMedia: @escaping (Bool) -> Void,
updateTransparency: @escaping (Int32) -> Void
) {
self.toggleEnabled = toggleEnabled
self.toggleArchiveMedia = toggleArchiveMedia
self.updateTransparency = updateTransparency
}
}
@@ -111,10 +143,12 @@ private final class DeletedMessagesControllerArguments {
private struct DeletedMessagesControllerState: Equatable {
var isEnabled: Bool
var archiveMedia: Bool
var transparencyPercent: Int32
static func ==(lhs: DeletedMessagesControllerState, rhs: DeletedMessagesControllerState) -> Bool {
return lhs.isEnabled == rhs.isEnabled &&
lhs.archiveMedia == rhs.archiveMedia
lhs.archiveMedia == rhs.archiveMedia &&
lhs.transparencyPercent == rhs.transparencyPercent
}
}
@@ -128,7 +162,8 @@ private func deletedMessagesControllerEntries(
entries.append(.enableToggle(presentationData.theme, "Сохранять удалённые сообщения", state.isEnabled))
entries.append(.archiveMediaToggle(presentationData.theme, "Архивировать медиа", state.archiveMedia))
entries.append(.settingsInfo(presentationData.theme, "Когда включено, сообщения, удалённые другими пользователями, будут сохраняться локально. Рядом со временем сообщения появится иконка корзины."))
entries.append(.transparencySlider(presentationData.theme, state.transparencyPercent, state.isEnabled))
entries.append(.settingsInfo(presentationData.theme, "Когда включено, сообщения, удалённые другими пользователями, будут сохраняться локально. Прозрачность влияет только на сообщения, которые уже помечены как удалённые."))
return entries
}
@@ -138,7 +173,8 @@ private func deletedMessagesControllerEntries(
public func deletedMessagesController(context: AccountContext) -> ViewController {
let initialState = DeletedMessagesControllerState(
isEnabled: AntiDeleteManager.shared.isEnabled,
archiveMedia: AntiDeleteManager.shared.archiveMedia
archiveMedia: AntiDeleteManager.shared.archiveMedia,
transparencyPercent: clampDeletedMessageTransparencyPercent(Int32(round(AntiDeleteManager.shared.deletedMessageTransparency * 100.0)))
)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
@@ -163,6 +199,15 @@ public func deletedMessagesController(context: AccountContext) -> ViewController
state.archiveMedia = value
return state
}
},
updateTransparency: { value in
let clampedValue = clampDeletedMessageTransparencyPercent(value)
AntiDeleteManager.shared.deletedMessageTransparency = Double(clampedValue) / 100.0
updateState { state in
var state = state
state.transparencyPercent = clampedValue
return state
}
}
)
@@ -195,3 +240,225 @@ public func deletedMessagesController(context: AccountContext) -> ViewController
let controller = ItemListController(context: context, state: signal)
return controller
}
private final class DeletedMessagesTransparencySliderItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let value: Int32
let isEnabled: Bool
let sectionId: ItemListSectionId
let updated: (Int32) -> Void
init(theme: PresentationTheme, value: Int32, isEnabled: Bool, sectionId: ItemListSectionId, updated: @escaping (Int32) -> Void) {
self.theme = theme
self.value = clampDeletedMessageTransparencyPercent(value)
self.isEnabled = isEnabled
self.sectionId = sectionId
self.updated = updated
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = DeletedMessagesTransparencySliderItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? DeletedMessagesTransparencySliderItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
private final class DeletedMessagesTransparencySliderItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let leftTextNode: ImmediateTextNode
private let rightTextNode: ImmediateTextNode
private let centerTextNode: ImmediateTextNode
private let slider = ComponentView<Empty>()
private var item: DeletedMessagesTransparencySliderItem?
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.leftTextNode = ImmediateTextNode()
self.rightTextNode = ImmediateTextNode()
self.centerTextNode = ImmediateTextNode()
super.init(layerBacked: false)
self.addSubnode(self.leftTextNode)
self.addSubnode(self.rightTextNode)
self.addSubnode(self.centerTextNode)
}
func asyncLayout() -> (_ item: DeletedMessagesTransparencySliderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
return { item, params, neighbors in
let separatorHeight = UIScreenPixel
let contentSize = CGSize(width: params.width, height: 88.0)
let insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.item = item
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = 0.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
let sideTextColor = item.theme.list.itemSecondaryTextColor.withAlphaComponent(item.isEnabled ? 1.0 : 0.6)
let centerTextColor = item.isEnabled ? item.theme.list.itemPrimaryTextColor : item.theme.list.itemDisabledTextColor
strongSelf.leftTextNode.attributedText = NSAttributedString(string: "Меньше", font: Font.regular(13.0), textColor: sideTextColor)
strongSelf.rightTextNode.attributedText = NSAttributedString(string: "Больше", font: Font.regular(13.0), textColor: sideTextColor)
strongSelf.centerTextNode.attributedText = NSAttributedString(string: "Прозрачность \(item.value)%", font: Font.regular(16.0), textColor: centerTextColor)
let leftTextSize = strongSelf.leftTextNode.updateLayout(CGSize(width: 120.0, height: 100.0))
let rightTextSize = strongSelf.rightTextNode.updateLayout(CGSize(width: 120.0, height: 100.0))
let centerTextSize = strongSelf.centerTextNode.updateLayout(CGSize(width: params.width - params.leftInset - params.rightInset - 60.0, height: 100.0))
let sideInset: CGFloat = 18.0
strongSelf.leftTextNode.frame = CGRect(origin: CGPoint(x: params.leftInset + sideInset, y: 15.0), size: leftTextSize)
strongSelf.rightTextNode.frame = CGRect(origin: CGPoint(x: params.width - params.leftInset - sideInset - rightTextSize.width, y: 15.0), size: rightTextSize)
strongSelf.centerTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - centerTextSize.width) / 2.0), y: 11.0), size: centerTextSize)
let maxRange = CGFloat(maxDeletedMessageTransparencyPercent - minDeletedMessageTransparencyPercent)
let normalizedValue: CGFloat
if maxRange.isZero {
normalizedValue = 0.0
} else {
normalizedValue = CGFloat(item.value - minDeletedMessageTransparencyPercent) / maxRange
}
let sliderSize = strongSelf.slider.update(
transition: .immediate,
component: AnyComponent(
SliderComponent(
content: .continuous(.init(
value: normalizedValue,
minValue: nil,
valueUpdated: { [weak self] value in
guard let self, let item = self.item, item.isEnabled else {
return
}
let transparencyValue = Int32((CGFloat(minDeletedMessageTransparencyPercent) + maxRange * value).rounded())
item.updated(clampDeletedMessageTransparencyPercent(transparencyValue))
}
)),
useNative: true,
trackBackgroundColor: item.theme.list.itemSwitchColors.frameColor,
trackForegroundColor: item.isEnabled ? item.theme.list.itemAccentColor : item.theme.list.itemDisabledTextColor
)
),
environment: {},
containerSize: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0)
)
if let sliderView = strongSelf.slider.view {
if sliderView.superview == nil {
strongSelf.view.addSubview(sliderView)
}
sliderView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - sliderSize.width) / 2.0), y: 36.0), size: sliderSize)
sliderView.isUserInteractionEnabled = item.isEnabled
sliderView.alpha = item.isEnabled ? 1.0 : 0.55
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}
@@ -19,6 +19,7 @@ private enum GhostgramSettingsEntry: ItemListNodeEntry {
case misc(PresentationTheme, String, String)
case deviceSpoof(PresentationTheme, String, String)
case voiceMorpher(PresentationTheme, String, String)
case sendDelay(PresentationTheme, String, String)
case info(PresentationTheme, String)
var section: ItemListSectionId {
@@ -37,8 +38,10 @@ private enum GhostgramSettingsEntry: ItemListNodeEntry {
return 3
case .voiceMorpher:
return 4
case .info:
case .sendDelay:
return 5
case .info:
return 6
}
}
@@ -74,6 +77,12 @@ private enum GhostgramSettingsEntry: ItemListNodeEntry {
return true
}
return false
case let .sendDelay(lhsTheme, lhsText, lhsValue):
if case let .sendDelay(rhsTheme, rhsText, rhsValue) = rhs,
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
}
return false
case let .info(lhsTheme, lhsText):
if case let .info(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
@@ -144,6 +153,17 @@ private enum GhostgramSettingsEntry: ItemListNodeEntry {
arguments.openVoiceMorpher()
}
)
case let .sendDelay(_, text, value):
return ItemListDisclosureItem(
presentationData: presentationData,
title: text,
label: value,
sectionId: self.section,
style: .blocks,
action: {
arguments.openSendDelay()
}
)
case let .info(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
}
@@ -158,19 +178,22 @@ private final class GhostgramSettingsControllerArguments {
let openMisc: () -> Void
let openDeviceSpoof: () -> Void
let openVoiceMorpher: () -> Void
let openSendDelay: () -> Void
init(
openDeletedMessages: @escaping () -> Void,
openGhostMode: @escaping () -> Void,
openMisc: @escaping () -> Void,
openDeviceSpoof: @escaping () -> Void,
openVoiceMorpher: @escaping () -> Void
openVoiceMorpher: @escaping () -> Void,
openSendDelay: @escaping () -> Void
) {
self.openDeletedMessages = openDeletedMessages
self.openGhostMode = openGhostMode
self.openMisc = openMisc
self.openDeviceSpoof = openDeviceSpoof
self.openVoiceMorpher = openVoiceMorpher
self.openSendDelay = openSendDelay
}
}
@@ -184,6 +207,8 @@ private struct GhostgramSettingsState: Equatable {
var miscActiveCount: Int
var deviceSpoofEnabled: Bool
var voiceMorpherEnabled: Bool
var voiceMorpherPresetName: String
var sendDelayEnabled: Bool
static func current() -> GhostgramSettingsState {
return GhostgramSettingsState(
@@ -193,7 +218,9 @@ private struct GhostgramSettingsState: Equatable {
miscEnabled: MiscSettingsManager.shared.isEnabled,
miscActiveCount: MiscSettingsManager.shared.activeFeatureCount,
deviceSpoofEnabled: DeviceSpoofManager.shared.isEnabled,
voiceMorpherEnabled: VoiceMorpherManager.shared.isEnabled
voiceMorpherEnabled: VoiceMorpherManager.shared.isEnabled,
voiceMorpherPresetName: VoiceMorpherManager.shared.selectedPreset.name,
sendDelayEnabled: SendDelayManager.shared.isEnabled
)
}
}
@@ -223,9 +250,13 @@ private func ghostgramSettingsControllerEntries(
entries.append(.deviceSpoof(presentationData.theme, "Подмена устройства", deviceSpoofStatus))
// Voice Morpher
let voiceMorpherStatus = state.voiceMorpherEnabled ? VoiceMorpherManager.shared.selectedPreset.name : "Выкл"
let voiceMorpherStatus = state.voiceMorpherEnabled ? state.voiceMorpherPresetName : "Выкл"
entries.append(.voiceMorpher(presentationData.theme, "Голосовой двойник", voiceMorpherStatus))
// Send Delay
let sendDelayStatus = state.sendDelayEnabled ? "Вкл" : "Выкл"
entries.append(.sendDelay(presentationData.theme, "Отложка сообщений", sendDelayStatus))
// Info
entries.append(.info(presentationData.theme, "Функции конфиденциальности Ghostgram. Скрытые отметки о прочтении, обход исчезающих сообщений, обход защиты от пересылки и другое."))
@@ -255,6 +286,9 @@ public func ghostgramSettingsController(context: AccountContext) -> ViewControll
},
openVoiceMorpher: {
pushControllerImpl?(voiceMorpherController(context: context), true)
},
openSendDelay: {
pushControllerImpl?(sendDelayController(context: context), true)
}
)
@@ -328,7 +328,7 @@ public func miscController(context: AccountContext) -> ViewController {
let controllerState = ItemListControllerState(
presentationData: ItemListPresentationData(presentationData),
title: .text("Misc"),
title: .text("Прочее"),
leftNavigationButton: nil,
rightNavigationButton: nil,
backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back),
@@ -90,7 +90,7 @@ class NotificationSearchItemNode: ListViewItemNode {
required init() {
self.searchBarNode = SearchBarPlaceholderNode()
super.init(layerBacked: false, dynamicBounce: false)
super.init(layerBacked: false)
self.addSubnode(self.searchBarNode)
}
@@ -104,16 +104,16 @@ class NotificationSearchItemNode: ListViewItemNode {
}
func asyncLayout() -> (_ item: NotificationSearchItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let searchBarNodeLayout = self.searchBarNode.asyncLayout()
let placeholder = self.placeholder
return { item, params in
let baseWidth = params.width - params.leftInset - params.rightInset
let backgroundColor = item.theme.chatList.itemBackgroundColor
let iconColor = UIColor(rgb: 0x8e8e93)
let controlColor = item.theme.chat.inputPanel.panelControlColor
let placeholderString = NSAttributedString(string: placeholder ?? "", font: searchBarFont, textColor: UIColor(rgb: 0x8e8e93))
let (_, searchBarApply) = searchBarNodeLayout(placeholderString, placeholderString, CGSize(width: baseWidth - 16.0, height: 28.0), 1.0, UIColor(rgb: 0x8e8e93), item.theme.chatList.regularSearchBarColor, backgroundColor, .immediate)
let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 44.0), insets: UIEdgeInsets())
@@ -126,10 +126,9 @@ class NotificationSearchItemNode: ListViewItemNode {
transition = .immediate
}
strongSelf.searchBarNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 8.0, y: 8.0), size: CGSize(width: baseWidth - 16.0, height: 28.0))
searchBarApply()
strongSelf.searchBarNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: baseWidth - 16.0, height: 28.0))
let searchBarSize = CGSize(width: baseWidth - 16.0, height: 28.0)
strongSelf.searchBarNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 8.0, y: 8.0), size: searchBarSize)
_ = strongSelf.searchBarNode.updateLayout(placeholderString: placeholderString, compactPlaceholderString: placeholderString, constrainedSize: searchBarSize, expansionProgress: 1.0, iconColor: iconColor, foregroundColor: item.theme.chatList.regularSearchBarColor, backgroundColor: backgroundColor, controlColor: controlColor, transition: transition)
transition.updateBackgroundColor(node: strongSelf, color: backgroundColor)
}
@@ -0,0 +1,148 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ItemListUI
import AccountContext
// MARK: - Section / Entry definitions
private enum SendDelaySection: Int32 {
case main
}
private enum SendDelayEntry: ItemListNodeEntry {
case toggle(PresentationTheme, String, Bool)
case info(PresentationTheme, String)
var section: ItemListSectionId {
return SendDelaySection.main.rawValue
}
var stableId: Int32 {
switch self {
case .toggle: return 0
case .info: return 1
}
}
static func ==(lhs: SendDelayEntry, rhs: SendDelayEntry) -> Bool {
switch lhs {
case let .toggle(lhsTheme, lhsText, lhsValue):
if case let .toggle(rhsTheme, rhsText, rhsValue) = rhs,
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
}
return false
case let .info(lhsTheme, lhsText):
if case let .info(rhsTheme, rhsText) = rhs,
lhsTheme === rhsTheme, lhsText == rhsText {
return true
}
return false
}
}
static func <(lhs: SendDelayEntry, rhs: SendDelayEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! SendDelayControllerArguments
switch self {
case let .toggle(_, text, value):
return ItemListSwitchItem(
presentationData: presentationData,
title: text,
value: value,
sectionId: self.section,
style: .blocks,
updated: { newValue in
arguments.toggleEnabled(newValue)
}
)
case let .info(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
}
}
}
// MARK: - Arguments
private final class SendDelayControllerArguments {
let toggleEnabled: (Bool) -> Void
init(toggleEnabled: @escaping (Bool) -> Void) {
self.toggleEnabled = toggleEnabled
}
}
// MARK: - State
private struct SendDelayControllerState: Equatable {
var isEnabled: Bool
}
// MARK: - Entries builder
private func sendDelayControllerEntries(
presentationData: PresentationData,
state: SendDelayControllerState
) -> [SendDelayEntry] {
let theme = presentationData.theme
return [
.toggle(theme, "Использовать отложку", state.isEnabled),
.info(theme, "Автоматически ставит задержку в ~12 секунд (дольше для сообщений с вложениями) при отправке сообщений. При использовании этой функции вы не будете появляться в сети.")
]
}
// MARK: - Controller
public func sendDelayController(context: AccountContext) -> ViewController {
let statePromise = ValuePromise(
SendDelayControllerState(isEnabled: SendDelayManager.shared.isEnabled),
ignoreRepeated: true
)
let stateValue = Atomic(value: SendDelayControllerState(isEnabled: SendDelayManager.shared.isEnabled))
let updateState: ((inout SendDelayControllerState) -> Void) -> Void = { f in
let result = stateValue.modify { state in
var s = state; f(&s); return s
}
statePromise.set(result)
}
let arguments = SendDelayControllerArguments(
toggleEnabled: { value in
SendDelayManager.shared.isEnabled = value
updateState { $0.isEnabled = value }
}
)
let signal = combineLatest(
context.sharedContext.presentationData,
statePromise.get()
)
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let entries = sendDelayControllerEntries(presentationData: presentationData, state: state)
let controllerState = ItemListControllerState(
presentationData: ItemListPresentationData(presentationData),
title: .text("Отложка сообщений"),
leftNavigationButton: nil,
rightNavigationButton: nil,
backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back),
animateChanges: false
)
let listState = ItemListNodeState(
presentationData: ItemListPresentationData(presentationData),
entries: entries,
style: .blocks,
animateChanges: true
)
return (controllerState, (listState, arguments))
}
return ItemListController(context: context, state: signal)
}
@@ -22,7 +22,7 @@ private let pink = UIColor(rgb: 0xef436c)
private let latePurple = UIColor(rgb: 0xaa56a6)
private let latePink = UIColor(rgb: 0xef476f)
private func textForTimeout(value: Int32) -> String {
private func callStatusBarTextForTimeout(value: Int32) -> String {
if value < 3600 {
let minutes = value / 60
let seconds = value % 60
@@ -498,9 +498,9 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
timerText = presentationData.strings.VoiceChat_StatusStartsIn(scheduledTimeIntervalString(strings: presentationData.strings, value: elapsedTime)).string
} else if elapsedTime < 0 {
isLate = true
timerText = presentationData.strings.VoiceChat_StatusLateBy(textForTimeout(value: abs(elapsedTime))).string
timerText = presentationData.strings.VoiceChat_StatusLateBy(callStatusBarTextForTimeout(value: abs(elapsedTime))).string
} else {
timerText = presentationData.strings.VoiceChat_StatusStartsIn(textForTimeout(value: elapsedTime)).string
timerText = presentationData.strings.VoiceChat_StatusStartsIn(callStatusBarTextForTimeout(value: elapsedTime)).string
}
segments.append(.text(0, NSAttributedString(string: timerText, font: textFont, textColor: textColor)))
} else if let membersCount = membersCount {
@@ -13,7 +13,7 @@ private let pink = UIColor(rgb: 0xef436c)
private let latePurple = UIColor(rgb: 0x974aa9)
private let latePink = UIColor(rgb: 0xf0436c)
private func textForTimeout(value: Int32) -> String {
private func scheduledInfoTextForTimeout(value: Int32) -> String {
if value < 3600 {
let minutes = value / 60
let seconds = value % 60
@@ -150,7 +150,7 @@ final class VideoChatScheduledInfoComponent: Component {
if remainingSeconds >= 86400 {
countdownText = scheduledTimeIntervalString(strings: component.strings, value: remainingSeconds)
} else {
countdownText = textForTimeout(value: abs(remainingSeconds))
countdownText = scheduledInfoTextForTimeout(value: abs(remainingSeconds))
/*if remainingSeconds < 0 && !self.isLate {
self.isLate = true
self.foregroundGradientLayer.colors = [latePink.cgColor, latePurple.cgColor, latePurple.cgColor]
@@ -12,7 +12,7 @@ private let pink = UIColor(rgb: 0xef436c)
private let latePurple = UIColor(rgb: 0x974aa9)
private let latePink = UIColor(rgb: 0xf0436c)
private func textForTimeout(value: Int32) -> String {
private func voiceChatTimerTextForTimeout(value: Int32) -> String {
if value < 3600 {
let minutes = value / 60
let seconds = value % 60
@@ -189,7 +189,7 @@ final class VoiceChatTimerNode: ASDisplayNode {
if elapsedTime >= 86400 {
timerText = scheduledTimeIntervalString(strings: self.strings, value: elapsedTime)
} else {
timerText = textForTimeout(value: abs(elapsedTime))
timerText = voiceChatTimerTextForTimeout(value: abs(elapsedTime))
if elapsedTime < 0 && !self.isLate {
self.isLate = true
self.foregroundGradientLayer.colors = [latePink.cgColor, latePurple.cgColor, latePurple.cgColor]
@@ -11,6 +11,7 @@ public final class AntiDeleteManager {
private let defaults = UserDefaults.standard
private let enabledKey = "antiDelete.enabled"
private let archiveMediaKey = "antiDelete.archiveMedia"
private let deletedMessageTransparencyKey = "antiDelete.deletedMessageTransparency"
private let archiveKey = "antiDelete.archive"
private let deletedIdsKey = "antiDelete.deletedIds"
@@ -26,6 +27,33 @@ public final class AntiDeleteManager {
set { defaults.set(newValue, forKey: archiveMediaKey) }
}
/// Минимальное значение прозрачности удалённого сообщения
public static let minDeletedMessageTransparency: Double = 0.0
/// Максимальное значение прозрачности удалённого сообщения
public static let maxDeletedMessageTransparency: Double = 0.8
/// Значение прозрачности удалённого сообщения по умолчанию
public static let defaultDeletedMessageTransparency: Double = 0.45
/// Прозрачность удалённых сообщений (0.0 = непрозрачно, 0.8 = максимально прозрачно)
public var deletedMessageTransparency: Double {
get {
let value = defaults.object(forKey: deletedMessageTransparencyKey) as? NSNumber
let resolvedValue = value?.doubleValue ?? Self.defaultDeletedMessageTransparency
return max(Self.minDeletedMessageTransparency, min(Self.maxDeletedMessageTransparency, resolvedValue))
}
set {
let clampedValue = max(Self.minDeletedMessageTransparency, min(Self.maxDeletedMessageTransparency, newValue))
defaults.set(clampedValue, forKey: deletedMessageTransparencyKey)
}
}
/// Альфа для отображения удалённых сообщений
public var deletedMessageDisplayAlpha: Double {
return 1.0 - self.deletedMessageTransparency
}
// MARK: - Deleted Message IDs Storage
private var deletedMessageIds: Set<String> = []
@@ -120,6 +148,9 @@ public final class AntiDeleteManager {
if defaults.object(forKey: archiveMediaKey) == nil {
defaults.set(true, forKey: archiveMediaKey)
}
if defaults.object(forKey: deletedMessageTransparencyKey) == nil {
defaults.set(Self.defaultDeletedMessageTransparency, forKey: deletedMessageTransparencyKey)
}
loadArchive()
loadDeletedIds()
}
@@ -23,9 +23,6 @@ public final class GhostModeManager {
private let defaults = UserDefaults.standard
// Prevents recursive mutual-exclusion calls
private var isApplyingMutualExclusion = false
// MARK: - Properties
/// Master toggle for Ghost Mode.
@@ -34,11 +31,9 @@ public final class GhostModeManager {
get { defaults.bool(forKey: Keys.isEnabled) }
set {
defaults.set(newValue, forKey: Keys.isEnabled)
if newValue && !isApplyingMutualExclusion {
// Ghost Mode ON disable Always Online
isApplyingMutualExclusion = true
if newValue {
// Ghost Mode ON disable Always Online so they don't coexist in UI
MiscSettingsManager.shared.disableAlwaysOnlineForMutualExclusion()
isApplyingMutualExclusion = false
}
notifySettingsChanged()
}
@@ -102,9 +97,11 @@ public final class GhostModeManager {
}
/// Online status is hidden only when Ghost Mode is on AND Always Online is NOT active.
/// Checks alwaysOnline raw value (not shouldAlwaysBeOnline) so ghost mode works
/// even when the Misc master toggle is off.
public var shouldHideOnlineStatus: Bool {
guard isEnabled && hideOnlineStatus else { return false }
return !MiscSettingsManager.shared.shouldAlwaysBeOnline
return !MiscSettingsManager.shared.alwaysOnline
}
public var shouldHideTypingIndicator: Bool {
@@ -114,7 +111,7 @@ public final class GhostModeManager {
/// Force offline only when Ghost Mode is on AND Always Online is NOT active.
public var shouldForceOffline: Bool {
guard isEnabled && forceOffline else { return false }
return !MiscSettingsManager.shared.shouldAlwaysBeOnline
return !MiscSettingsManager.shared.alwaysOnline
}
/// Count of active features (e.g., "5/5")
@@ -131,17 +128,6 @@ public final class GhostModeManager {
/// Total number of features
public static let totalFeatureCount = 5
// MARK: - Internal mutual exclusion (called by MiscSettingsManager)
/// Called by MiscSettingsManager when Always Online is turned on.
/// Disables Ghost Mode without triggering mutual exclusion back.
public func disableForMutualExclusion() {
isApplyingMutualExclusion = true
defaults.set(false, forKey: Keys.isEnabled)
notifySettingsChanged()
isApplyingMutualExclusion = false
}
// MARK: - Initialization
private init() {
@@ -16,9 +16,6 @@ public final class MiscSettingsManager {
private let defaults = UserDefaults.standard
// Prevents recursive mutual-exclusion calls
private var isApplyingMutualExclusion = false
// MARK: - Main Toggle
public var isEnabled: Bool {
@@ -68,17 +65,12 @@ public final class MiscSettingsManager {
}
/// Always appear as online.
/// Enabling this automatically disables Ghost Mode (mutual exclusion).
/// NOTE: Ghost Mode features dynamically yield to Always Online via their
/// `shouldAlwaysBeOnline` check, so no permanent disabling is needed here.
public var alwaysOnline: Bool {
get { defaults.bool(forKey: Keys.alwaysOnline) }
set {
defaults.set(newValue, forKey: Keys.alwaysOnline)
if newValue && !isApplyingMutualExclusion {
// Always Online ON disable Ghost Mode
isApplyingMutualExclusion = true
GhostModeManager.shared.disableForMutualExclusion()
isApplyingMutualExclusion = false
}
notifySettingsChanged()
}
}
@@ -122,7 +114,7 @@ public final class MiscSettingsManager {
disableViewOnceAutoDelete = true
bypassScreenshotProtection = true
blockAds = true
alwaysOnline = true // setter handles mutual exclusion
alwaysOnline = true
}
public func disableAll() {
@@ -136,12 +128,10 @@ public final class MiscSettingsManager {
// MARK: - Internal mutual exclusion (called by GhostModeManager)
/// Called by GhostModeManager when Ghost Mode is turned on.
/// Disables Always Online without triggering mutual exclusion back.
/// Disables Always Online so the two modes don't coexist in the UI.
public func disableAlwaysOnlineForMutualExclusion() {
isApplyingMutualExclusion = true
defaults.set(false, forKey: Keys.alwaysOnline)
notifySettingsChanged()
isApplyingMutualExclusion = false
}
// MARK: - Notification
@@ -365,14 +365,46 @@ public func enqueueMessages(account: Account, peerId: PeerId, messages: [Enqueue
} else {
signal = .single(messages.map { (false, $0) })
}
let hasMedia = messages.contains { message in
if case let .message(_, _, _, mediaReference, _, _, _, _, _, _) = message {
return mediaReference != nil
}
return false
}
// GHOSTGRAM: Send delay write to Postbox immediately so the UI
// clears the input field, then delay _only_ the return signal.
// The actual network send delay is handled by scheduling: we add
// OutgoingScheduleInfoMessageAttribute inside the transaction so
// the message is stored as "scheduled" and Telegram server sends it
// after the delay elapses. The message appears in Scheduled Messages
// section for the duration of the delay.
return signal
|> mapToSignal { messages -> Signal<[MessageId?], NoError> in
return account.postbox.transaction { transaction -> [MessageId?] in
return enqueueMessages(transaction: transaction, account: account, peerId: peerId, messages: messages)
var finalMessages = messages
if SendDelayManager.shared.isEnabled {
let delayInterval = hasMedia
? SendDelayManager.mediaDelaySeconds
: SendDelayManager.textDelaySeconds
let scheduleTime = Int32(Date().timeIntervalSince1970) + Int32(delayInterval)
finalMessages = messages.map { (transformed, msg) in
let updatedMsg = msg.withUpdatedAttributes { attrs in
var attrs = attrs
attrs.removeAll(where: { $0 is OutgoingScheduleInfoMessageAttribute })
attrs.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: scheduleTime, repeatPeriod: nil))
return attrs
}
return (transformed, updatedMsg)
}
}
return enqueueMessages(transaction: transaction, account: account, peerId: peerId, messages: finalMessages)
}
}
}
public func enqueueMessagesToMultiplePeers(account: Account, peerIds: [PeerId], threadIds: [PeerId: Int64], messages: [EnqueueMessage]) -> Signal<[MessageId], NoError> {
let signal: Signal<[(Bool, EnqueueMessage)], NoError>
if let transformOutgoingMessageMedia = account.transformOutgoingMessageMedia {
@@ -0,0 +1,55 @@
import Foundation
/// SendDelayManager - delays outgoing messages by ~12 seconds to prevent
/// online status from appearing after sending.
///
/// Delays are applied per-message at the enqueueMessages level.
/// Media messages receive a slightly longer delay (~20 s) because upload
/// time would otherwise reveal the send moment anyway.
public final class SendDelayManager {
// MARK: - Singleton
public static let shared = SendDelayManager()
// MARK: - UserDefaults Keys
private enum Keys {
static let isEnabled = "SendDelay.isEnabled"
}
// MARK: - Storage
private let defaults = UserDefaults.standard
// MARK: - Properties
/// When true, all outgoing messages are delayed before being enqueued.
public var isEnabled: Bool {
get { defaults.bool(forKey: Keys.isEnabled) }
set {
defaults.set(newValue, forKey: Keys.isEnabled)
notifySettingsChanged()
}
}
// MARK: - Delay constants
/// Base delay for text-only messages.
public static let textDelaySeconds: Double = 12.0
/// Delay for messages that contain media attachments.
public static let mediaDelaySeconds: Double = 20.0
// MARK: - Init
private init() {}
// MARK: - Notifications
public static let settingsChangedNotification = Notification.Name("SendDelaySettingsChanged")
private func notifySettingsChanged() {
NotificationCenter.default.post(name: SendDelayManager.settingsChangedNotification, object: nil)
}
}
@@ -4233,6 +4233,9 @@ func replayFinalState(
if AntiDeleteManager.shared.isEnabled {
let messageIds = transaction.messageIdsForGlobalIds(ids)
for (index, messageId) in messageIds.enumerated() {
// Skip scheduled/local/quick-reply messages they get deleted when sent, not by the remote peer
guard messageId.namespace == Namespaces.Message.Cloud else { continue }
if let message = transaction.getMessage(messageId) {
let globalId = index < ids.count ? ids[index] : 0
@@ -4289,6 +4292,9 @@ func replayFinalState(
if AntiDeleteManager.shared.isEnabled {
let messageIds = transaction.messageIdsForGlobalIds(ids)
for messageId in messageIds {
// Skip scheduled/local/quick-reply messages they get deleted when sent, not by the remote peer
guard messageId.namespace == Namespaces.Message.Cloud else { continue }
// Mark as deleted for icon display
AntiDeleteManager.shared.markAsDeleted(peerId: messageId.peerId.toInt64(), messageId: messageId.id)
@@ -4317,6 +4323,9 @@ func replayFinalState(
// ANTI-DELETE: Archive channel messages with full content before deletion
if AntiDeleteManager.shared.isEnabled {
for messageId in ids {
// Skip scheduled/local/quick-reply messages they get deleted when sent, not by the remote peer
guard messageId.namespace == Namespaces.Message.Cloud else { continue }
if let message = transaction.getMessage(messageId) {
// Extract text content
let textContent = message.text
@@ -4370,6 +4379,9 @@ func replayFinalState(
// ANTI-DELETE: Mark messages as deleted instead of removing them
if AntiDeleteManager.shared.isEnabled {
for messageId in ids {
// Skip scheduled/local/quick-reply messages they get deleted when sent, not by the remote peer
guard messageId.namespace == Namespaces.Message.Cloud else { continue }
// Mark as deleted for icon display
AntiDeleteManager.shared.markAsDeleted(peerId: messageId.peerId.toInt64(), messageId: messageId.id)
@@ -82,16 +82,19 @@ private final class AccountPresenceManagerImpl {
/// 2. Ghost Mode hide online status skip update entirely (freeze last-seen)
/// 3. Default app behaviour (wasOnline)
private func refreshPresence() {
let alwaysOnline = MiscSettingsManager.shared.shouldAlwaysBeOnline
// Use raw alwaysOnline flag (not shouldAlwaysBeOnline) so it works independently
// of the Misc master toggle. Ghost Mode's shouldHideOnlineStatus already checks
// !MiscSettingsManager.shared.alwaysOnline internally.
let alwaysOnline = MiscSettingsManager.shared.alwaysOnline
let ghostHideOnline = GhostModeManager.shared.shouldHideOnlineStatus
if alwaysOnline {
// Always Online wins push online regardless of Ghost Mode
sendPresenceUpdate(online: true)
} else if ghostHideOnline {
// Ghost Mode active, no Always Online freeze presence (don't send anything)
self.onlineTimer?.invalidate()
self.onlineTimer = nil
// Ghost Mode active: actively send offline so the server immediately
// hides our last-seen instead of keeping the stale "online" status.
sendPresenceUpdate(online: false)
} else {
// Normal mode follow the app-level state
sendPresenceUpdate(online: wasOnline)
@@ -27,7 +27,6 @@ import MediaEditor
import AvatarBackground
import LottieComponent
import UndoUI
import PremiumAlertController
public struct AvatarKeyboardInputData: Equatable {
var emoji: EmojiPagerContentComponent
@@ -481,6 +481,10 @@ private func mapVisibility(_ visibility: ListViewItemNodeVisibility, boundsSize:
}
}
private func isDeletedBubbleMessage(_ message: Message) -> Bool {
return AntiDeleteManager.shared.isMessageDeleted(peerId: message.id.peerId.toInt64(), messageId: message.id.id) || AntiDeleteManager.shared.isMessageDeleted(text: message.text)
}
public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode {
public class ContentContainer {
public let contentMessageStableId: UInt32
@@ -4368,6 +4372,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
}
let deletedMessageAlpha = CGFloat(AntiDeleteManager.shared.deletedMessageDisplayAlpha)
var deletedMessageStableIds = Set<UInt32>()
for (message, _) in item.content {
if isDeletedBubbleMessage(message) {
deletedMessageStableIds.insert(message.stableId)
}
}
var incomingOffset: CGFloat = 0.0
switch backgroundType {
case .incoming:
@@ -4512,10 +4524,31 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
contentContainer?.update(size: relativeFrame.size, contentOrigin: contentOrigin, selectionInsets: selectionInsets, index: index, presentationData: item.presentationData, graphics: graphics, backgroundType: backgroundType, presentationContext: item.controllerInteraction.presentationContext, mediaBox: item.context.account.postbox.mediaBox, messageSelection: itemSelection)
if let contentContainer = contentContainer {
let containerAlpha: CGFloat = deletedMessageStableIds.contains(stableId) ? deletedMessageAlpha : 1.0
if case .System = animation {
animation.animator.updateAlpha(layer: contentContainer.sourceNode.contentNode.layer, alpha: containerAlpha, completion: nil)
} else {
contentContainer.sourceNode.contentNode.alpha = containerAlpha
}
}
index += 1
}
let mainContainerAlpha: CGFloat
if contentContainerNodeFrames.isEmpty, !deletedMessageStableIds.isEmpty {
mainContainerAlpha = deletedMessageAlpha
} else {
mainContainerAlpha = 1.0
}
if case .System = animation {
animation.animator.updateAlpha(layer: strongSelf.mainContextSourceNode.contentNode.layer, alpha: mainContainerAlpha, completion: nil)
} else {
strongSelf.mainContextSourceNode.contentNode.alpha = mainContainerAlpha
}
if hasSelection {
var currentMaskView: UIImageView?
if let maskView = strongSelf.contentContainersWrapperNode.view.mask as? UIImageView {
@@ -29,6 +29,27 @@ public final class NavigationButtonComponent: Component {
case more
case icon(imageName: String)
case proxy(status: ChatTitleProxyStatus)
/// Liquid glass avatar button for account switching.
/// peerId is used as a diff key; avatarImage is the rendered avatar.
case avatar(peerId: String, avatarImage: UIImage?)
public static func ==(lhs: Content, rhs: Content) -> Bool {
switch (lhs, rhs) {
case let (.text(lt, lb), .text(rt, rb)):
return lt == rt && lb == rb
case (.more, .more):
return true
case let (.icon(l), .icon(r)):
return l == r
case let (.proxy(l), .proxy(r)):
return l == r
case let (.avatar(lId, _), .avatar(rId, _)):
// Re-render when peerId changes; image updates are handled by the view itself
return lId == rId
default:
return false
}
}
}
public let content: Content
@@ -62,6 +83,12 @@ public final class NavigationButtonComponent: Component {
private var moreButton: MoreHeaderButton?
// MARK: - Liquid Glass Avatar
private var avatarContainerView: UIView?
private var avatarBlurView: UIVisualEffectView?
private var avatarImageView: UIImageView?
private var avatarBorderLayer: CAShapeLayer?
private var component: NavigationButtonComponent?
private var theme: PresentationTheme?
@@ -74,19 +101,23 @@ public final class NavigationButtonComponent: Component {
guard let self else {
return
}
if highlighted {
self.textView?.alpha = 0.6
self.proxyNode?.alpha = 0.6
self.iconView?.alpha = 0.6
} else {
self.textView?.alpha = 1.0
self.textView?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2)
self.proxyNode?.alpha = 1.0
self.proxyNode?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2)
self.iconView?.alpha = 1.0
self.iconView?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2)
let alpha: CGFloat = highlighted ? 0.55 : 1.0
self.textView?.alpha = alpha
self.proxyNode?.alpha = alpha
self.iconView?.alpha = alpha
self.avatarContainerView?.alpha = alpha
if !highlighted {
let animateAlpha = { (layer: CALayer?) in
let anim = CABasicAnimation(keyPath: "opacity")
anim.fromValue = 0.55
anim.toValue = 1.0
anim.duration = 0.2
layer?.add(anim, forKey: "opacity")
}
animateAlpha(self.textView?.layer)
animateAlpha(self.proxyNode?.layer)
animateAlpha(self.iconView?.layer)
animateAlpha(self.avatarContainerView?.layer)
}
}
}
@@ -99,6 +130,51 @@ public final class NavigationButtonComponent: Component {
self.component?.pressed(self)
}
// MARK: - Liquid glass avatar setup
private func setupAvatarViewsIfNeeded() {
guard avatarContainerView == nil else { return }
// Container holds blur + image
let container = UIView()
container.isUserInteractionEnabled = false
container.clipsToBounds = true
// Blur background liquid glass effect
let blurEffect = UIBlurEffect(style: .systemUltraThinMaterial)
let blurView = UIVisualEffectView(effect: blurEffect)
blurView.isUserInteractionEnabled = false
container.addSubview(blurView)
// Avatar image on top of blur
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.isUserInteractionEnabled = false
imageView.clipsToBounds = true
container.addSubview(imageView)
self.addSubview(container)
self.avatarContainerView = container
self.avatarBlurView = blurView
self.avatarImageView = imageView
// Subtle glass ring border
let borderLayer = CAShapeLayer()
borderLayer.fillColor = UIColor.clear.cgColor
borderLayer.strokeColor = UIColor.white.withAlphaComponent(0.22).cgColor
borderLayer.lineWidth = 1.5
container.layer.addSublayer(borderLayer)
self.avatarBorderLayer = borderLayer
}
private func removeAvatarViews() {
avatarContainerView?.removeFromSuperview()
avatarContainerView = nil
avatarBlurView = nil
avatarImageView = nil
avatarBorderLayer = nil
}
func update(component: NavigationButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<NavigationButtonComponentEnvironment>, transition: ComponentTransition) -> CGSize {
self.component = component
@@ -113,6 +189,7 @@ public final class NavigationButtonComponent: Component {
var imageName: String?
var proxyStatus: ChatTitleProxyStatus?
var isMore: Bool = false
var avatarContent: (peerId: String, image: UIImage?)? = nil
switch component.content {
case let .text(title, isBold):
@@ -123,10 +200,13 @@ public final class NavigationButtonComponent: Component {
imageName = imageNameValue
case let .proxy(status):
proxyStatus = status
case let .avatar(peerId, image):
avatarContent = (peerId, image)
}
var size = CGSize(width: 0.0, height: availableSize.height)
// MARK: Text
if let textString = textString {
let textView: ImmediateTextView
if let current = self.textView {
@@ -144,11 +224,13 @@ public final class NavigationButtonComponent: Component {
size.width = max(44.0, textSize.width + textInset * 2.0)
textView.frame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: floor((availableSize.height - textSize.height) / 2.0)), size: textSize)
removeAvatarViews()
} else if let textView = self.textView {
self.textView = nil
textView.removeFromSuperview()
}
// MARK: Icon
if let imageName = imageName {
let iconView: UIImageView
if let current = self.iconView {
@@ -166,15 +248,16 @@ public final class NavigationButtonComponent: Component {
if let iconSize = iconView.image?.size {
size.width = 44.0
iconView.frame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((availableSize.height - iconSize.height) / 2.0)), size: iconSize)
}
removeAvatarViews()
} else if let iconView = self.iconView {
self.iconView = nil
iconView.removeFromSuperview()
self.iconImageName = nil
}
// MARK: Proxy
if let proxyStatus = proxyStatus {
let proxyNode: ChatTitleProxyNode
if let current = self.proxyNode {
@@ -191,13 +274,14 @@ public final class NavigationButtonComponent: Component {
proxyNode.theme = theme
proxyNode.status = proxyStatus
proxyNode.frame = CGRect(origin: CGPoint(x: floor((size.width - proxySize.width) / 2.0), y: floor((availableSize.height - proxySize.height) / 2.0)), size: proxySize)
removeAvatarViews()
} else if let proxyNode = self.proxyNode {
self.proxyNode = nil
proxyNode.removeFromSupernode()
}
// MARK: More
if isMore {
let moreButton: MoreHeaderButton
if let current = self.moreButton, !themeUpdated {
@@ -233,13 +317,52 @@ public final class NavigationButtonComponent: Component {
size.width = 44.0
moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: theme.rootController.navigationBar.buttonColor)))
moreButton.frame = CGRect(origin: CGPoint(x: floor((size.width - buttonSize.width) / 2.0), y: floor((size.height - buttonSize.height) / 2.0)), size: buttonSize)
removeAvatarViews()
} else if let moreButton = self.moreButton {
self.moreButton = nil
moreButton.removeFromSupernode()
}
// MARK: Liquid Glass Avatar
if let (_, image) = avatarContent {
setupAvatarViewsIfNeeded()
let avatarDiameter: CGFloat = 28.0
size.width = 44.0
let containerRect = CGRect(
x: floor((size.width - avatarDiameter) / 2.0),
y: floor((availableSize.height - avatarDiameter) / 2.0),
width: avatarDiameter,
height: avatarDiameter
)
avatarContainerView?.frame = containerRect
avatarContainerView?.layer.cornerRadius = avatarDiameter / 2.0
avatarBlurView?.frame = CGRect(origin: .zero, size: containerRect.size)
if let image = image {
avatarImageView?.image = image
avatarImageView?.frame = CGRect(origin: .zero, size: containerRect.size)
avatarImageView?.backgroundColor = nil
} else {
avatarImageView?.image = nil
// Fallback: solid tinted background when no photo
avatarImageView?.frame = CGRect(origin: .zero, size: containerRect.size)
avatarImageView?.backgroundColor = theme.list.itemAccentColor.withAlphaComponent(0.35)
}
// Update border ring path
let borderPath = UIBezierPath(roundedRect: CGRect(origin: .zero, size: containerRect.size).insetBy(dx: 0.75, dy: 0.75), cornerRadius: avatarDiameter / 2.0)
avatarBorderLayer?.path = borderPath.cgPath
avatarBorderLayer?.frame = CGRect(origin: .zero, size: containerRect.size)
} else if avatarContent == nil && avatarContainerView != nil {
removeAvatarViews()
}
return size
}
}
@@ -5,6 +5,8 @@ swift_library(
module_name = "GiftViewScreen",
srcs = glob([
"Sources/**/*.swift",
], exclude = [
"Sources/TableComponent.swift",
]),
copts = [
"-warnings-as-errors",
@@ -133,7 +133,7 @@ public func giftOfferAlertController(
HStack(items, spacing: 4.0)
)
tableItems.append(.init(
tableItems.append(TableComponent.Item(
id: id,
title: title,
hasBackground: false,
@@ -180,12 +180,12 @@ public func giftOfferAlertController(
AlertTextComponent(content: .plain(text))
)
))
content.append(AnyComponentWithIdentity(
let tableComponent = AnyComponent(AlertTableComponent(items: tableItems))
let tableEntry = AnyComponentWithIdentity<AlertComponentEnvironment>(
id: "table",
component: AnyComponent(
AlertTableComponent(items: tableItems)
)
))
component: tableComponent
)
content.append(tableEntry)
if let valueAmount = gift.valueUsdAmount {
let resaleConfiguration = StarsSubscriptionConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
@@ -118,7 +118,7 @@ public func giftTransferAlertController(
HStack(items, spacing: 4.0)
)
tableItems.append(.init(
tableItems.append(TableComponent.Item(
id: id,
title: title,
hasBackground: false,
@@ -165,12 +165,12 @@ public func giftTransferAlertController(
AlertTextComponent(content: .plain(text))
)
))
content.append(AnyComponentWithIdentity(
let tableComponent = AnyComponent(AlertTableComponent(items: tableItems))
let tableEntry = AnyComponentWithIdentity<AlertComponentEnvironment>(
id: "table",
component: AnyComponent(
AlertTableComponent(items: tableItems)
)
))
component: tableComponent
)
content.append(tableEntry)
let alertController = ChatMessagePaymentAlertController(
context: context,
@@ -8,7 +8,7 @@ import ChatPresentationInterfaceState
import AsyncDisplayKit
import AccountContext
open class ChatTitleAccessoryPanelNode: ASDisplayNode {
open class LegacyChatTitleAccessoryPanelNode: ASDisplayNode {
public typealias LayoutResult = ChatControllerCustomNavigationPanelNodeLayoutResult
open var interfaceInteraction: ChatPanelInterfaceInteraction?
@@ -19,11 +19,11 @@ open class ChatTitleAccessoryPanelNode: ASDisplayNode {
}
public final class LegacyChatHeaderPanelComponent: Component {
public let panelNode: ChatTitleAccessoryPanelNode
public let panelNode: LegacyChatTitleAccessoryPanelNode
public let interfaceState: ChatPresentationInterfaceState
public init(
panelNode: ChatTitleAccessoryPanelNode,
panelNode: LegacyChatTitleAccessoryPanelNode,
interfaceState: ChatPresentationInterfaceState
) {
self.panelNode = panelNode
@@ -232,20 +232,34 @@ final class AffiliateProgramSetupScreenComponent: Component {
)
))
let tableItems: [TableComponent.Item] = [
TableComponent.Item(id: 0, title: environment.strings.AffiliateSetup_AlertApply_SectionCommission, component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: commissionTitle, font: Font.regular(15.0), textColor: environment.theme.actionSheet.primaryTextColor))
))),
TableComponent.Item(id: 1, title: environment.strings.AffiliateSetup_AlertApply_SectionDuration, component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: durationTitle, font: Font.regular(15.0), textColor: environment.theme.actionSheet.primaryTextColor))
)))
]
content.append(AnyComponentWithIdentity(
let textColor = environment.theme.actionSheet.primaryTextColor
let commissionItemText = NSAttributedString(
string: commissionTitle,
font: Font.regular(15.0),
textColor: textColor
)
let durationItemText = NSAttributedString(
string: durationTitle,
font: Font.regular(15.0),
textColor: textColor
)
let commissionItem = TableComponent.Item(
id: 0,
title: environment.strings.AffiliateSetup_AlertApply_SectionCommission,
component: AnyComponent(MultilineTextComponent(text: .plain(commissionItemText)))
)
let durationItem = TableComponent.Item(
id: 1,
title: environment.strings.AffiliateSetup_AlertApply_SectionDuration,
component: AnyComponent(MultilineTextComponent(text: .plain(durationItemText)))
)
let tableItems: [TableComponent.Item] = [commissionItem, durationItem]
let tableComponent = AnyComponent(AlertTableComponent(items: tableItems))
let tableEntry = AnyComponentWithIdentity<AlertComponentEnvironment>(
id: "table",
component: AnyComponent(
AlertTableComponent(items: tableItems)
)
))
component: tableComponent
)
content.append(tableEntry)
let alertController = AlertScreen(
context: component.context,
@@ -6,7 +6,7 @@ import TelegramPresentationData
import MultilineTextComponent
import AlertComponent
final class TableComponent: CombinedComponent {
final class AffiliateTableComponent: CombinedComponent {
class Item: Equatable {
public let id: AnyHashable
public let title: String
@@ -45,7 +45,7 @@ final class TableComponent: CombinedComponent {
self.items = items
}
public static func ==(lhs: TableComponent, rhs: TableComponent) -> Bool {
public static func ==(lhs: AffiliateTableComponent, rhs: AffiliateTableComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
@@ -228,9 +228,9 @@ private final class TableAlertContentComponent: CombinedComponent {
let theme: PresentationTheme
let title: String
let text: String
let table: TableComponent
let table: AffiliateTableComponent
init(theme: PresentationTheme, title: String, text: String, table: TableComponent) {
init(theme: PresentationTheme, title: String, text: String, table: AffiliateTableComponent) {
self.theme = theme
self.title = title
self.text = text
@@ -256,7 +256,7 @@ private final class TableAlertContentComponent: CombinedComponent {
public static var body: Body {
let title = Child(MultilineTextComponent.self)
let text = Child(MultilineTextComponent.self)
let table = Child(TableComponent.self)
let table = Child(AffiliateTableComponent.self)
return { context in
let title = title.update(
@@ -318,17 +318,3 @@ private final class TableAlertContentComponent: CombinedComponent {
}
}
}
func tableAlert(theme: PresentationTheme, title: String, text: String, table: TableComponent, actions: [ComponentAlertAction]) -> ViewController {
return componentAlertController(
theme: AlertControllerTheme(presentationTheme: theme, fontSize: .regular),
content: AnyComponent(TableAlertContentComponent(
theme: theme,
title: title,
text: text,
table: table
)),
actions: actions,
actionLayout: .horizontal
)
}
@@ -8,7 +8,8 @@ import TelegramPresentationData
import ProgressNavigationButtonNode
import AccountContext
import SearchUI
import ChatListUI
import func ChatListUI.chatListFilterItems
import enum ChatListUI.ChatListContainerNodeFilter
import CounterControllerTitleView
import ChatListFilterTabContainerNode
@@ -69,7 +69,7 @@ public final class BirthdayPickerComponent: Component {
private let calendar = Calendar(identifier: .gregorian)
private var value = TelegramBirthday(day: 1, month: 1, year: nil)
private var minYear: Int32 = 1900
private var minYear: Int32 = 1
private let maxYear: Int32
override init(frame: CGRect) {
@@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.6581 5.56807C9.26054 4.9489 10.0877 4.59961 10.9516 4.59961H18.4004C20.1677 4.59961 21.6004 6.0323 21.6004 7.79961V16.1996C21.6004 17.9669 20.1677 19.3996 18.4004 19.3996H10.9516C10.0877 19.3996 9.26054 19.0503 8.6581 18.4311L3.486 13.1154C2.88168 12.4943 2.88168 11.5049 3.486 10.8838L8.6581 5.56807Z" stroke="black" stroke-width="1.66"/>
<path d="M11.5996 9.2002L17.1996 14.8002" stroke="black" stroke-width="1.66" stroke-linecap="round"/>
<path d="M11.5996 14.7998L17.1996 9.1998" stroke="black" stroke-width="1.66" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 659 B

@@ -1,8 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="9.2" stroke="black" stroke-width="1.66"/>
<ellipse cx="12" cy="12" rx="5.2" ry="9.2" stroke="black" stroke-width="1.328"/>
<path d="M12 2.8V21.2" stroke="black" stroke-width="1.328"/>
<path d="M2.8 12L21.2 12" stroke="black" stroke-width="1.328"/>
<path d="M4.4 17.6C4.4 17.6 6.75325 16 12 16C17.2468 16 19.6 17.6 19.6 17.6" stroke="black" stroke-width="1.328"/>
<path d="M19.6 6.4C19.6 6.4 17.2468 8 12 8C6.75325 8 4.4 6.4 4.4 6.4" stroke="black" stroke-width="1.328"/>
</svg>

Before

Width:  |  Height:  |  Size: 601 B

@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.0626 21.9306H12.9374C13.652 21.9306 14.1996 21.494 14.3666 20.8065L14.7657 19.0693L15.0627 18.9672L16.5755 19.8961C17.1787 20.277 17.8748 20.1841 18.3852 19.6732L19.6846 18.3819C20.195 17.871 20.2879 17.165 19.9073 16.5704L18.9607 15.0656L19.0721 14.7868L20.8076 14.3781C21.4851 14.2109 21.9306 13.6535 21.9306 12.9475V11.1082C21.9306 10.4022 21.4944 9.84484 20.8076 9.67761L19.0906 9.2596L18.9699 8.96231L19.9166 7.45739C20.2971 6.86287 20.2043 6.16613 19.6939 5.64595L18.3945 4.34539C17.8934 3.84375 17.1973 3.75086 16.594 4.12244L15.0812 5.05138L14.7657 4.93066L14.3666 3.19348C14.1996 2.50605 13.652 2.06944 12.9374 2.06944H11.0626C10.348 2.06944 9.80046 2.50605 9.63335 3.19348L9.22503 4.93066L8.90949 5.05138L7.40598 4.12244C6.80272 3.75086 6.09735 3.84375 5.5962 4.34539L4.30614 5.64595C3.79569 6.16613 3.6936 6.86287 4.0834 7.45739L5.02077 8.96231L4.90939 9.2596L3.19243 9.67761C2.50565 9.84484 2.06945 10.4022 2.06945 11.1082V12.9475C2.06945 13.6535 2.51493 14.2109 3.19243 14.3781L4.92794 14.7868L5.03005 15.0656L4.09268 16.5704C3.70289 17.165 3.80498 17.871 4.31542 18.3819L5.60548 19.6732C6.1159 20.1841 6.82127 20.277 7.42453 19.8961L8.92804 18.9672L9.22503 19.0693L9.63335 20.8065C9.80046 21.494 10.348 21.9306 11.0626 21.9306ZM11.2111 20.4814C11.0534 20.4814 10.9698 20.4164 10.942 20.2677L10.3851 17.9639C9.81901 17.8246 9.28997 17.6016 8.89093 17.3508L6.86766 18.5956C6.75627 18.6792 6.63567 18.6606 6.52428 18.5492L5.42915 17.453C5.32704 17.3508 5.31776 17.2393 5.39198 17.1092L6.63567 15.1027C6.42217 14.7126 6.1809 14.1831 6.03241 13.6164L3.73073 13.0683C3.58223 13.0404 3.51727 12.9568 3.51727 12.7989V11.2475C3.51727 11.0803 3.57295 11.006 3.73073 10.9781L6.02313 10.4208C6.17163 9.81694 6.45 9.26888 6.61711 8.92515L5.3827 6.91859C5.29921 6.77926 5.30849 6.66775 5.41059 6.55631L6.515 5.47873C6.62639 5.36722 6.72844 5.34867 6.86766 5.43228L8.87232 6.6492C9.27142 6.42625 9.83757 6.19402 10.3944 6.03607L10.942 3.73228C10.9698 3.58365 11.0534 3.51862 11.2111 3.51862H12.7889C12.9466 3.51862 13.0302 3.58365 13.0487 3.73228L13.6149 6.05469C14.1903 6.2033 14.6915 6.43553 15.1091 6.65848L17.1231 5.43228C17.2716 5.34867 17.3643 5.36722 17.485 5.47873L18.5801 6.55631C18.6915 6.66775 18.6915 6.77926 18.608 6.91859L17.3737 8.92515C17.55 9.26888 17.8191 9.81694 17.9676 10.4208L20.2693 10.9781C20.4178 11.006 20.4827 11.0803 20.4827 11.2475V12.7989C20.4827 12.9568 20.4085 13.0404 20.2693 13.0683L17.9583 13.6164C17.8098 14.1831 17.5778 14.7126 17.3551 15.1027L18.5987 17.1092C18.673 17.2393 18.673 17.3508 18.5616 17.453L17.4757 18.5492C17.3551 18.6606 17.2437 18.6792 17.1231 18.5956L15.0998 17.3508C14.7007 17.6016 14.181 17.8246 13.6149 17.9639L13.0487 20.2677C13.0302 20.4164 12.9466 20.4814 12.7889 20.4814H11.2111ZM12 15.5486C13.9397 15.5486 15.536 13.9508 15.536 12C15.536 10.0678 13.9397 8.46997 12 8.46997C10.0603 8.46997 8.45472 10.0678 8.45472 12C8.45472 13.9415 10.051 15.5486 12 15.5486ZM12 14.1087C10.8491 14.1087 9.90251 13.1612 9.90251 12C9.90251 10.8574 10.8491 9.90984 12 9.90984C13.1323 9.90984 14.0789 10.8574 14.0789 12C14.0789 13.1519 13.1323 14.1087 12 14.1087Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

@@ -1,3 +0,0 @@
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M22 4.515C12.3433 4.515 4.515 12.3433 4.515 22C4.515 31.6567 12.3433 39.485 22 39.485C31.6567 39.485 39.485 31.6567 39.485 22C39.485 12.3433 31.6567 4.515 22 4.515ZM1.485 22C1.485 10.6699 10.6699 1.485 22 1.485C33.3301 1.485 42.515 10.6699 42.515 22C42.515 33.3301 33.3301 42.515 22 42.515C10.6699 42.515 1.485 33.3301 1.485 22ZM21.9986 13.152C22.8353 13.152 23.5136 13.8303 23.5136 14.667V20.4854H29.3319C30.1686 20.4854 30.8469 21.1636 30.8469 22.0004C30.8469 22.8371 30.1686 23.5154 29.3319 23.5154H23.5136V29.3337C23.5136 30.1704 22.8353 30.8487 21.9986 30.8487C21.1619 30.8487 20.4836 30.1704 20.4836 29.3337V23.5154H14.6652C13.8285 23.5154 13.1502 22.8371 13.1502 22.0004C13.1502 21.1636 13.8285 20.4854 14.6652 20.4854H20.4836V14.667C20.4836 13.8303 21.1619 13.152 21.9986 13.152Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 956 B

@@ -692,6 +692,44 @@ extension ChatControllerImpl {
self.displayNode = ChatControllerNode(context: self.context, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, subject: self.subject, controllerInteraction: self.controllerInteraction!, chatPresentationInterfaceState: self.presentationInterfaceState, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, navigationBar: self.navigationBar, statusBar: self.statusBar, backgroundNode: self.chatBackgroundNode, controller: self)
// Seed the title pill synchronously from whatever data is available at this point,
// so the header shows the peer name immediately instead of staying blank until
// the first async contentDataUpdated() fires.
let initialTitleContent: ChatTitleContent? = self.contentData?.state.chatTitleContent
?? self.presentationInterfaceState.renderedPeer.flatMap { renderedPeer -> ChatTitleContent? in
guard let peer = renderedPeer.peer else { return nil }
let peerData = ChatTitleContent.PeerData(
peerId: renderedPeer.peerId,
peer: peer,
isContact: false,
isSavedMessages: peer.id == self.context.account.peerId,
notificationSettings: nil,
peerPresences: [:],
cachedData: nil
)
return .peer(
peerView: peerData,
customTitle: nil,
customSubtitle: nil,
onlineMemberCount: (nil, nil),
isScheduledMessages: false,
isMuted: nil,
customMessageCount: nil,
isEnabled: true
)
}
if let initialTitleContent {
self.chatTitleView?.update(
context: self.context,
theme: self.presentationData.theme,
strings: self.presentationData.strings,
dateTimeFormat: self.presentationData.dateTimeFormat,
nameDisplayOrder: self.presentationData.nameDisplayOrder,
content: initialTitleContent,
transition: .immediate
)
}
if let currentItem = self.globalControlPanelsContext?.tempVoicePlaylistCurrentItem {
self.chatDisplayNode.historyNode.voicePlaylistItemChanged(nil, currentItem)
}
@@ -146,7 +146,6 @@ import ChatTextInputPanelNode
import ChatInputAccessoryPanel
import GlobalControlPanelsContext
import ChatSearchNavigationContentNode
import ChatAgeRestrictionAlertController
public final class ChatControllerOverlayPresentationData {
public let expandData: (ASDisplayNode?, () -> Void)
@@ -830,7 +829,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return true
}
let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] message, params in
let openMessage: (Message, OpenMessageParams) -> Bool = { [weak self] message, params in
guard let self, self.isNodeLoaded, let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(message.id) else {
return false
}
@@ -1566,7 +1565,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.controllerInteraction?.isOpeningMediaSignal = openChatMessageParams.blockInteraction.get()
return context.sharedContext.openChatMessage(openChatMessageParams)
}, openPeer: { [weak self] peer, navigation, fromMessage, source in
}
let openPeer: (EnginePeer, ChatControllerInteractionNavigateToPeer, MessageReference?, ChatControllerInteraction.OpenPeerSource) -> Void = { [weak self] peer, navigation, fromMessage, source in
var expandAvatar = false
if case let .groupParticipant(storyStats, avatarHeaderNode) = source {
if let storyStats, storyStats.totalCount != 0, let avatarHeaderNode = avatarHeaderNode as? ChatMessageAvatarHeaderNodeImpl {
@@ -1581,20 +1582,28 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
fromReactionMessageId = fromMessage?.id
}
self?.openPeer(peer: peer, navigation: navigation, fromMessage: fromMessage, fromReactionMessageId: fromReactionMessageId, expandAvatar: expandAvatar)
}, openPeerMention: { [weak self] name, progress in
}
let openPeerMention: (String, Promise<Bool>?) -> Void = { [weak self] name, progress in
self?.openPeerMention(name, progress: progress)
}, openMessageContextMenu: { [weak self] message, selectAll, node, frame, anyRecognizer, location in
}
let openMessageContextMenu: (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?, CGPoint?) -> Void = { [weak self] message, selectAll, node, frame, anyRecognizer, location in
guard let self, self.isNodeLoaded else {
return
}
self.openMessageContextMenu(message: message, selectAll: selectAll, node: node, frame: frame, anyRecognizer: anyRecognizer, location: location)
}, openMessageReactionContextMenu: { [weak self] message, sourceView, gesture, value in
}
let openMessageReactionContextMenu: (Message, ContextExtractedContentContainingView, ContextGesture?, MessageReaction.Reaction) -> Void = { [weak self] message, sourceView, gesture, value in
guard let self else {
return
}
self.openMessageReactionContextMenu(message: message, sourceView: sourceView, gesture: gesture, value: value)
}, updateMessageReaction: { [weak self] initialMessage, reaction, force, sourceView in
}
let updateMessageReaction: (Message, ChatControllerInteractionReaction, Bool, ContextExtractedContentContainingView?) -> Void = { [weak self] initialMessage, reaction, force, sourceView in
guard let strongSelf = self else {
return
}
@@ -2062,7 +2071,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
})
}, activateMessagePinch: { [weak self] sourceNode in
}
let controllerInteraction = ChatControllerInteraction(
openMessage: openMessage,
openPeer: openPeer,
openPeerMention: openPeerMention,
openMessageContextMenu: openMessageContextMenu,
openMessageReactionContextMenu: openMessageReactionContextMenu,
updateMessageReaction: updateMessageReaction,
activateMessagePinch: { [weak self] sourceNode in
guard let strongSelf = self else {
return
}
@@ -893,7 +893,8 @@ extension ChatControllerImpl {
peerVerification = cachedChannelData.verification
}
}
copyProtectionEnabled = peer.isCopyProtectionEnabled
// GHOSTGRAM: Bypass copy protection if enabled in Misc settings
copyProtectionEnabled = MiscSettingsManager.shared.shouldBypassCopyProtection ? false : peer.isCopyProtectionEnabled
if let cachedGroupData = peerView.cachedData as? CachedGroupData {
if !cachedGroupData.botInfos.isEmpty {
hasBots = true
@@ -1371,7 +1372,8 @@ extension ChatControllerImpl {
var alwaysShowGiftButton = false
var disallowedGifts: TelegramDisallowedGifts?
if let peer = peerView.peers[peerView.peerId] {
copyProtectionEnabled = peer.isCopyProtectionEnabled
// GHOSTGRAM: Bypass copy protection if enabled in Misc settings
copyProtectionEnabled = MiscSettingsManager.shared.shouldBypassCopyProtection ? false : peer.isCopyProtectionEnabled
if let cachedData = peerView.cachedData as? CachedUserData {
contactStatus = ChatContactStatus(canAddContact: !peerView.peerIsContact, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: nil, managingBot: managingBot)
if case let .known(value) = cachedData.businessIntro {
@@ -1129,7 +1129,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
}
}
let isSecret = self.chatPresentationInterfaceState.copyProtectionEnabled || self.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat || self.chatLocation.peerId?.isVerificationCodes == true
// GHOSTGRAM: Bypass screenshot protection if enabled in Misc settings
let effectiveCopyProtection = MiscSettingsManager.shared.shouldBypassScreenshotProtection ? false : self.chatPresentationInterfaceState.copyProtectionEnabled
let isSecret = effectiveCopyProtection || self.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat || self.chatLocation.peerId?.isVerificationCodes == true
if self.historyNodeContainer.isSecret != isSecret {
self.historyNodeContainer.isSecret = isSecret
setLayerDisableScreenshots(self.titleAccessoryPanelContainer.layer, isSecret)
@@ -4600,12 +4602,10 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
}
}
self.setupSendActionOnViewUpdate({ [weak self] in
guard let self, let textInputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode else {
return
}
// GHOSTGRAM: When send delay is active, scheduled messages
// don't trigger history view update, so clear input immediately.
if SendDelayManager.shared.isEnabled {
self.collapseInput()
self.ignoreUpdateHeight = true
textInputPanelNode.text = ""
self.requestUpdateChatInterfaceState(.immediate, overrideThreadId == nil, { state in
@@ -4624,7 +4624,33 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
return state
})
self.ignoreUpdateHeight = false
}, usedCorrelationId)
} else {
self.setupSendActionOnViewUpdate({ [weak self] in
guard let self, let textInputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode else {
return
}
self.collapseInput()
self.ignoreUpdateHeight = true
textInputPanelNode.text = ""
self.requestUpdateChatInterfaceState(.immediate, overrideThreadId == nil, { state in
var state = state
state = state.withUpdatedReplyMessageSubject(nil)
state = state.withUpdatedSendMessageEffect(nil)
if state.postSuggestionState != nil {
state = state.withUpdatedPostSuggestionState(nil)
state = state.withUpdatedEditMessage(nil)
}
state = state.withUpdatedForwardMessageIds(nil)
state = state.withUpdatedForwardOptionsState(nil)
state = state.withUpdatedComposeDisableUrlPreviews([])
return state
})
self.ignoreUpdateHeight = false
}, usedCorrelationId)
}
completion()
self.sendMessages(messages, silentPosting, scheduleTime, repeatPeriod, messages.count > 1, postpone)
@@ -2056,6 +2056,10 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
}
}
}
// GHOSTGRAM: Bypass copy protection if enabled in Misc settings
if MiscSettingsManager.shared.shouldBypassCopyProtection {
isCopyProtectionEnabled = false
}
let alwaysDisplayTranscribeButton = ChatMessageItemAssociatedData.DisplayTranscribeButton(
canBeDisplayed: suggestAudioTranscription.0 < 2,
displayForNotConsumed: suggestAudioTranscription.1,
@@ -2102,7 +2106,8 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
selectedMessages: selectedMessages,
presentationData: chatPresentationData,
historyAppearsCleared: historyAppearsCleared,
skipViewOnceMedia: mode != .bubbles,
// GHOSTGRAM: Keep view-once media visible if bypass is enabled
skipViewOnceMedia: MiscSettingsManager.shared.shouldDisableViewOnceAutoDelete ? false : (mode != .bubbles),
pendingUnpinnedAllMessages: pendingUnpinnedAllMessages,
pendingRemovedMessages: pendingRemovedMessages,
associatedData: associatedData,
@@ -2110,8 +2115,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
customChannelDiscussionReadState: customChannelDiscussionReadState,
customThreadOutgoingReadState: customThreadOutgoingReadState,
cachedData: data.cachedData,
adMessage: allAdMessages.fixed,
dynamicAdMessages: allAdMessages.opportunistic
// GHOSTGRAM: Block ads if enabled in Misc settings
adMessage: MiscSettingsManager.shared.shouldBlockAds ? nil : allAdMessages.fixed,
dynamicAdMessages: MiscSettingsManager.shared.shouldBlockAds ? [] : allAdMessages.opportunistic
)
let lastHeaderId = filteredEntries.last.flatMap { listMessageDateHeaderId(timestamp: $0.index.timestamp) } ?? 0
let processedView = ChatHistoryView(originalView: view, filteredEntries: filteredEntries, associatedData: associatedData, lastHeaderId: lastHeaderId, id: id, locationInput: update.2, ignoreMessagesInTimestampRange: update.3, ignoreMessageIds: update.4)
@@ -31,7 +31,7 @@ final class ChatSearchNavigationContentNode: NavigationBarContentNode {
self.chatLocation = chatLocation
self.interaction = interaction
self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasBackground: false, hasSeparator: false), strings: strings, fieldStyle: .modern)
self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasBackground: false, hasSeparator: false), presentationTheme: theme, strings: strings, fieldStyle: .modern)
let placeholderText: String
switch chatLocation {
case .peer, .replyThread, .customChatContents:
@@ -90,10 +90,11 @@ final class ChatSearchNavigationContentNode: NavigationBarContentNode {
return 54.0
}
override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
let searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - self.nominalHeight), size: CGSize(width: size.width, height: 54.0))
self.searchBar.frame = searchBarFrame
self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: leftInset, rightInset: rightInset, transition: transition)
return size
}
func activate() {
@@ -106,7 +107,7 @@ final class ChatSearchNavigationContentNode: NavigationBarContentNode {
func update(presentationInterfaceState: ChatPresentationInterfaceState) {
if let search = presentationInterfaceState.search {
self.searchBar.updateThemeAndStrings(theme: SearchBarNodeTheme(theme: presentationInterfaceState.theme, hasBackground: false, hasSeparator: false), strings: presentationInterfaceState.strings)
self.searchBar.updateThemeAndStrings(theme: SearchBarNodeTheme(theme: presentationInterfaceState.theme, hasBackground: false, hasSeparator: false), presentationTheme: presentationInterfaceState.theme, strings: presentationInterfaceState.strings)
switch search.domain {
case .everything, .tag:
@@ -1,16 +1,4 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ChatPresentationInterfaceState
import AccountContext
import class LegacyChatHeaderPanelComponent.LegacyChatTitleAccessoryPanelNode
class ChatTitleAccessoryPanelNode: ASDisplayNode {
typealias LayoutResult = ChatControllerCustomNavigationPanelNode.LayoutResult
var interfaceInteraction: ChatPanelInterfaceInteraction?
func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> LayoutResult {
preconditionFailure()
}
class ChatTitleAccessoryPanelNode: LegacyChatTitleAccessoryPanelNode {
}
+2 -1
View File
@@ -49,6 +49,8 @@ genrule(
BUILD_ARCH="arm64"
elif [ "$(TARGET_CPU)" == "ios_sim_arm64" ]; then
BUILD_ARCH="sim_arm64"
elif [ "$(TARGET_CPU)" == "ios_x86_64" ]; then
BUILD_ARCH="sim_x86_64"
else
echo "Unsupported architecture $(TARGET_CPU)"
fi
@@ -119,4 +121,3 @@ objc_library(
"//visibility:public",
],
)
+7 -1
View File
@@ -18,6 +18,13 @@ elif [ "$ARCH" = "sim_arm64" ]; then
custom_xcode_path="$(xcode-select -p)/"
sed -i '' "s|/Applications/Xcode.app/Contents/Developer/|$custom_xcode_path|g" "$TARGET_CROSSFILE"
CROSSFILE="../package/crossfiles/arm64-iPhoneSimulator-custom.meson"
elif [ "$ARCH" = "sim_x86_64" ]; then
TARGET_CROSSFILE="$BUILD_DIR/dav1d/package/crossfiles/x86_64-iPhoneSimulator-custom.meson"
cp "$BUILD_DIR/dav1d/package/crossfiles/x86_64-iPhoneSimulator.meson" "$TARGET_CROSSFILE"
custom_xcode_path="$(xcode-select -p)/"
sed -i '' "s|/Applications/Xcode.app/Contents/Developer/|$custom_xcode_path|g" "$TARGET_CROSSFILE"
CROSSFILE="../package/crossfiles/x86_64-iPhoneSimulator-custom.meson"
MESON_OPTIONS="$MESON_OPTIONS -Denable_asm=false"
else
echo "Unsupported architecture $ARCH"
exit 1
@@ -33,4 +40,3 @@ ninja
popd
popd
+16
View File
@@ -37,6 +37,22 @@ elif [ "$ARCH" = "sim_arm64" ]; then
echo "set(CMAKE_SYSTEM_PROCESSOR aarch64)" >> toolchain.cmake
echo "set(CMAKE_C_COMPILER $(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang)" >> toolchain.cmake
cmake -G"Unix Makefiles" -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake -DCMAKE_OSX_SYSROOT=${IOS_SYSROOT[0]} -DPNG_SUPPORTED=FALSE -DENABLE_SHARED=FALSE -DWITH_JPEG8=1 -DBUILD=10000 -DCMAKE_POLICY_VERSION_MINIMUM=3.5 ../mozjpeg
make
elif [ "$ARCH" = "x86_64" ]; then
IOS_PLATFORMDIR="$(xcode-select -p)/Platforms/iPhoneSimulator.platform"
IOS_SYSROOT=($IOS_PLATFORMDIR/Developer/SDKs/iPhoneSimulator*.sdk)
export CFLAGS="-Wall -arch x86_64 --target=x86_64-apple-ios13.0-simulator -miphonesimulator-version-min=13.0 -funwind-tables"
cd "$BUILD_DIR"
mkdir build
cd build
touch toolchain.cmake
echo "set(CMAKE_SYSTEM_NAME Darwin)" >> toolchain.cmake
echo "set(CMAKE_SYSTEM_PROCESSOR x86_64)" >> toolchain.cmake
echo "set(CMAKE_C_COMPILER $(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang)" >> toolchain.cmake
cmake -G"Unix Makefiles" -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake -DCMAKE_OSX_SYSROOT=${IOS_SYSROOT[0]} -DPNG_SUPPORTED=FALSE -DENABLE_SHARED=FALSE -DWITH_JPEG8=1 -DBUILD=10000 -DCMAKE_POLICY_VERSION_MINIMUM=3.5 ../mozjpeg
make
else
+8 -1
View File
@@ -38,8 +38,15 @@ genrule(
BUILD_ARCH="arm64"
elif [ "$(TARGET_CPU)" == "ios_sim_arm64" ]; then
BUILD_ARCH="sim_arm64"
elif [ "$(TARGET_CPU)" == "ios_x86_64" ]; then
BUILD_ARCH="sim_x86_64"
elif [ "$(TARGET_CPU)" == "darwin_arm64" ]; then
BUILD_ARCH="sim_arm64"
elif [ "$(TARGET_CPU)" == "darwin_x86_64" ]; then
BUILD_ARCH="sim_x86_64"
else
echo "Unsupported architecture $(TARGET_CPU)"
echo "Unsupported architecture $(TARGET_CPU)" >&2
exit 1
fi
BUILD_DIR="$(RULEDIR)/build_$${BUILD_ARCH}"
+60 -5
View File
@@ -17,36 +17,91 @@ options="$options -DOPENSSL_INCLUDE_DIR=${OPENSSL_DIR}/src/include"
options="$options -DCMAKE_BUILD_TYPE=Release"
options="$options -DIOS_DEPLOYMENT_TARGET=13.0"
# Bazel genrule runs with PATH=/bin:/usr/bin, so resolve CPU count without
# relying on /usr/sbin being in PATH.
if [ -n "${TD_BUILD_JOBS:-}" ]; then
BUILD_JOBS="$TD_BUILD_JOBS"
elif [ -x /usr/sbin/sysctl ]; then
BUILD_JOBS="$(/usr/sbin/sysctl -n hw.ncpu)"
elif command -v getconf >/dev/null 2>&1; then
BUILD_JOBS="$(getconf _NPROCESSORS_ONLN 2>/dev/null || true)"
fi
case "$BUILD_JOBS" in
''|*[!0-9]*)
BUILD_JOBS=8
;;
esac
if [ "$BUILD_JOBS" -lt 1 ]; then
BUILD_JOBS=1
fi
MAX_BUILD_JOBS="${TD_MAX_BUILD_JOBS:-8}"
case "$MAX_BUILD_JOBS" in
''|*[!0-9]*)
MAX_BUILD_JOBS=8
;;
esac
if [ "$MAX_BUILD_JOBS" -lt 1 ]; then
MAX_BUILD_JOBS=8
fi
if [ "$BUILD_JOBS" -gt "$MAX_BUILD_JOBS" ]; then
BUILD_JOBS="$MAX_BUILD_JOBS"
fi
if [ -z "$BUILD_JOBS" ]; then
BUILD_JOBS=8
fi
cd "$BUILD_DIR"
# Generate source files
mkdir native-build
cd native-build
cmake -DTD_GENERATE_SOURCE_FILES=ON ../td
cmake --build . -- -j$(sysctl -n hw.ncpu)
cmake --build . -- -j"$BUILD_JOBS"
cd ..
if [ "$ARCH" = "arm64" ]; then
IOS_PLATFORMDIR="$(xcode-select -p)/Platforms/iPhoneOS.platform"
IOS_SYSROOT=($IOS_PLATFORMDIR/Developer/SDKs/iPhoneOS*.sdk)
export CFLAGS="-arch arm64 --target=arm64-apple-ios13.0 -miphoneos-version-min=13.0"
cmake_arch="arm64"
clang_target="arm64-apple-ios13.0"
minimum_target_flag="-miphoneos-version-min=13.0"
cmake_processor="aarch64"
elif [ "$ARCH" = "sim_arm64" ]; then
IOS_PLATFORMDIR="$(xcode-select -p)/Platforms/iPhoneSimulator.platform"
IOS_SYSROOT=($IOS_PLATFORMDIR/Developer/SDKs/iPhoneSimulator*.sdk)
export CFLAGS="-arch arm64 --target=arm64-apple-ios13.0-simulator -miphonesimulator-version-min=13.0"
cmake_arch="arm64"
clang_target="arm64-apple-ios13.0-simulator"
minimum_target_flag="-miphonesimulator-version-min=13.0"
cmake_processor="aarch64"
elif [ "$ARCH" = "sim_x86_64" ]; then
IOS_PLATFORMDIR="$(xcode-select -p)/Platforms/iPhoneSimulator.platform"
IOS_SYSROOT=($IOS_PLATFORMDIR/Developer/SDKs/iPhoneSimulator*.sdk)
cmake_arch="x86_64"
clang_target="x86_64-apple-ios13.0-simulator"
minimum_target_flag="-miphonesimulator-version-min=13.0"
cmake_processor="x86_64"
else
echo "Unsupported architecture $ARCH"
exit 1
fi
export CFLAGS="-arch ${cmake_arch} --target=${clang_target} ${minimum_target_flag}"
export CXXFLAGS="$CFLAGS"
export LDFLAGS="$CFLAGS"
# Common build steps
mkdir build
cd build
touch toolchain.cmake
echo "set(CMAKE_SYSTEM_NAME Darwin)" >> toolchain.cmake
echo "set(CMAKE_SYSTEM_PROCESSOR aarch64)" >> toolchain.cmake
echo "set(CMAKE_SYSTEM_PROCESSOR ${cmake_processor})" >> toolchain.cmake
echo "set(CMAKE_OSX_ARCHITECTURES ${cmake_arch})" >> toolchain.cmake
echo "set(CMAKE_C_COMPILER $(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang)" >> toolchain.cmake
echo "set(CMAKE_CXX_COMPILER $(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang++)" >> toolchain.cmake
echo "set(CMAKE_C_COMPILER_TARGET ${clang_target})" >> toolchain.cmake
echo "set(CMAKE_CXX_COMPILER_TARGET ${clang_target})" >> toolchain.cmake
cmake -G"Unix Makefiles" -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake -DCMAKE_OSX_SYSROOT=${IOS_SYSROOT[0]} ../td $options
make tde2e -j$(sysctl -n hw.ncpu)
make tde2e -j"$BUILD_JOBS"