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/ /codesigning/
/build-system/real-codesigning/ /build-system/real-codesigning/
/build-system/local-codesigning/ /build-system/local-codesigning/
build-output/
build-output/
+14 -1
View File
@@ -36,6 +36,7 @@ load(
"telegram_bazel_path", "telegram_bazel_path",
"telegram_use_xcode_managed_codesigning", "telegram_use_xcode_managed_codesigning",
"telegram_bundle_id", "telegram_bundle_id",
"telegram_is_appstore_build",
"telegram_aps_environment", "telegram_aps_environment",
"telegram_team_id", "telegram_team_id",
"telegram_enable_icloud", "telegram_enable_icloud",
@@ -509,6 +510,16 @@ aps_fragment = "" if telegram_aps_environment == "" else """
<string>{telegram_aps_environment}</string> <string>{telegram_aps_environment}</string>
""".format(telegram_aps_environment=telegram_aps_environment) """.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 = """ app_groups_fragment = """
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
@@ -546,6 +557,8 @@ plist_fragment(
extension = "entitlements", extension = "entitlements",
template = "".join([ template = "".join([
aps_fragment, aps_fragment,
beta_reports_active_fragment,
get_task_allow_fragment,
app_groups_fragment, app_groups_fragment,
siri_fragment, siri_fragment,
associated_domains_fragment, associated_domains_fragment,
@@ -1718,7 +1731,7 @@ ios_application(
":RequiredDeviceCapabilitiesPlist", ":RequiredDeviceCapabilitiesPlist",
":UrlTypesInfoPlist", ":UrlTypesInfoPlist",
], ],
app_icons = [ ":{}_icon".format(name) for name in composer_icon_folders ], app_icons = [":DefaultAppIcon"],
alternate_icons = [ alternate_icons = [
":{}".format(name) for name in alternate_icon_folders ":{}".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", "filename" : "GhostIcon@40x40.png",
"idiom": "iphone", "idiom" : "iphone",
"scale": "2x", "scale" : "2x",
"size": "20x20" "size" : "20x20"
}, },
{ {
"filename": "GhostIcon@60x60.png", "filename" : "GhostIcon@60x60.png",
"idiom": "iphone", "idiom" : "iphone",
"scale": "3x", "scale" : "3x",
"size": "20x20" "size" : "20x20"
}, },
{ {
"filename": "GhostIcon@58x58.png", "filename" : "GhostIcon@58x58.png",
"idiom": "iphone", "idiom" : "iphone",
"scale": "2x", "scale" : "2x",
"size": "29x29" "size" : "29x29"
}, },
{ {
"filename": "GhostIcon@87x87.png", "filename" : "GhostIcon@87x87.png",
"idiom": "iphone", "idiom" : "iphone",
"scale": "3x", "scale" : "3x",
"size": "29x29" "size" : "29x29"
}, },
{ {
"filename": "GhostIcon@80x80.png", "filename" : "GhostIcon@80x80.png",
"idiom": "iphone", "idiom" : "iphone",
"scale": "2x", "scale" : "2x",
"size": "40x40" "size" : "40x40"
}, },
{ {
"filename": "GhostIcon@120x120.png", "filename" : "GhostIcon@120x120.png",
"idiom": "iphone", "idiom" : "iphone",
"scale": "3x", "scale" : "3x",
"size": "40x40" "size" : "40x40"
}, },
{ {
"filename": "GhostIcon@120x120.png", "filename" : "GhostIcon@120x120.png",
"idiom": "iphone", "idiom" : "iphone",
"scale": "2x", "scale" : "2x",
"size": "60x60" "size" : "60x60"
}, },
{ {
"filename": "GhostIcon@180x180.png", "filename" : "GhostIcon@180x180.png",
"idiom": "iphone", "idiom" : "iphone",
"scale": "3x", "scale" : "3x",
"size": "60x60" "size" : "60x60"
}, },
{ {
"filename": "GhostIcon@20x20.png", "filename" : "GhostIcon@40x40.png",
"idiom": "ipad", "idiom" : "ipad",
"scale": "1x", "scale" : "2x",
"size": "20x20" "size" : "20x20"
}, },
{ {
"filename": "GhostIcon@40x40.png", "filename" : "GhostIcon@58x58.png",
"idiom": "ipad", "idiom" : "ipad",
"scale": "2x", "scale" : "2x",
"size": "20x20" "size" : "29x29"
}, },
{ {
"filename": "GhostIcon@29x29.png", "filename" : "GhostIcon@80x80.png",
"idiom": "ipad", "idiom" : "ipad",
"scale": "1x", "scale" : "2x",
"size": "29x29" "size" : "40x40"
}, },
{ {
"filename": "GhostIcon@58x58.png", "filename" : "GhostIcon@152x152.png",
"idiom": "ipad", "idiom" : "ipad",
"scale": "2x", "scale" : "2x",
"size": "29x29" "size" : "76x76"
}, },
{ {
"filename": "GhostIcon@40x40.png", "filename" : "GhostIcon@167x167.png",
"idiom": "ipad", "idiom" : "ipad",
"scale": "1x", "scale" : "2x",
"size": "40x40" "size" : "83.5x83.5"
}, },
{ {
"filename": "GhostIcon@80x80.png", "filename" : "GhostIcon@1024x1024.png",
"idiom": "ipad", "idiom" : "ios-marketing",
"scale": "2x", "scale" : "1x",
"size": "40x40" "size" : "1024x1024"
},
{
"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"
} }
], ],
"info": { "info" : {
"author": "xcode", "author" : "xcode",
"version": 1 "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 += ', '
arguments_array += '[[NSString alloc] initWithFormat:@"%@", arg{}]'.format(i) arguments_array += '[[NSString alloc] initWithFormat:@"%@", arg{}]'.format(i)
formatted_accessors += ''' 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}) {{ uint32_t keyId{arguments_string}) {{
NSString *formatString = getSingle(strings, strings->_idToKey[@(keyId)], nil); NSString *formatString = getSingle(strings, strings->_idToKey[@(keyId)], nil);
NSArray<_FormattedStringRange *> *argumentRanges = extractArgumentRanges(formatString); NSArray<_FormattedStringRange *> *argumentRanges = extractArgumentRanges(formatString);
+5
View File
@@ -232,6 +232,9 @@ class BazelCommandLine:
combined_arguments += self.common_debug_args combined_arguments += self.common_debug_args
combined_arguments += self.get_define_arguments() combined_arguments += self.get_define_arguments()
if self.disable_provisioning_profiles:
combined_arguments += ['--//Telegram:disableProvisioningProfiles']
if self.remote_cache is not None: if self.remote_cache is not None:
combined_arguments += [ combined_arguments += [
'--remote_cache={}'.format(self.remote_cache), '--remote_cache={}'.format(self.remote_cache),
@@ -559,6 +562,8 @@ def generate_project(bazel, arguments):
disable_extensions = arguments.disableExtensions disable_extensions = arguments.disableExtensions
if arguments.disableProvisioningProfiles is not None: if arguments.disableProvisioningProfiles is not None:
disable_provisioning_profiles = arguments.disableProvisioningProfiles disable_provisioning_profiles = arguments.disableProvisioningProfiles
if disable_provisioning_profiles:
bazel_command_line.set_disable_provisioning_profiles()
if arguments.projectIncludeRelease is not None: if arguments.projectIncludeRelease is not None:
project_include_release = arguments.projectIncludeRelease project_include_release = arguments.projectIncludeRelease
if arguments.xcodeManagedCodesigning is not None and arguments.xcodeManagedCodesigning == True: 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 target_name == 'Telegram':
if disable_extensions: if disable_extensions:
bazel_generate_arguments += ['--//{}:disableExtensions'.format(app_target)] 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)] bazel_generate_arguments += ['--//{}:disableStripping'.format(app_target)]
project_bazel_arguments = [] project_bazel_arguments = []
@@ -35,6 +37,8 @@ def generate_xcodeproj(build_environment: BuildEnvironment, disable_extensions,
if target_name == 'Telegram': if target_name == 'Telegram':
if disable_extensions: if disable_extensions:
project_bazel_arguments += ['--//{}:disableExtensions'.format(app_target)] 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 += ['--//{}:disableStripping'.format(app_target)]
project_bazel_arguments += ['--features=-swift.debug_prefix_map'] project_bazel_arguments += ['--features=-swift.debug_prefix_map']
+1 -1
View File
@@ -4,7 +4,7 @@
"api_hash": "", "api_hash": "",
"team_id": "", "team_id": "",
"app_center_id": "0", "app_center_id": "0",
"is_internal_build": "false", "is_internal_build": "true",
"is_appstore_build": "true", "is_appstore_build": "true",
"appstore_id": "0", "appstore_id": "0",
"app_specific_url_scheme": "ghostgram", "app_specific_url_scheme": "ghostgram",
+2
View File
@@ -16,9 +16,11 @@ objc_library(
], allow_empty=True) + private_headers, ], allow_empty=True) + private_headers,
copts = [ copts = [
"-Werror", "-Werror",
"-Wno-deprecated-declarations",
], ],
cxxopts = [ cxxopts = [
"-Werror", "-Werror",
"-Wno-deprecated-declarations",
"-std=c++17", "-std=c++17",
], ],
hdrs = public_headers, hdrs = public_headers,
@@ -89,7 +89,7 @@ public class ChatListSearchItemNode: ListViewItemNode {
required public init() { required public init() {
self.searchBarNode = SearchBarPlaceholderNode(fieldStyle: .modern) self.searchBarNode = SearchBarPlaceholderNode(fieldStyle: .modern)
super.init(layerBacked: false, dynamicBounce: false) super.init(layerBacked: false)
self.addSubnode(self.searchBarNode) 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) { public func asyncLayout() -> (_ item: ChatListSearchItem, _ params: ListViewItemLayoutParams, _ nextIsPinned: Bool, _ isEnabled: Bool) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let searchBarNodeLayout = self.searchBarNode.asyncLayout()
let placeholder = self.placeholder let placeholder = self.placeholder
return { [weak self] item, params, nextIsPinned, isEnabled in 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 backgroundColor = nextIsPinned ? item.theme.chatList.pinnedItemBackgroundColor : item.theme.chatList.itemBackgroundColor
let placeholderColor = item.theme.list.itemSecondaryTextColor let placeholderColor = item.theme.list.itemSecondaryTextColor
let controlColor = item.theme.chat.inputPanel.panelControlColor
let placeholderString = NSAttributedString(string: placeholder ?? "", font: searchBarFont, textColor: placeholderColor) 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()) 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)) 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 strongSelf.searchBarNode.frame = searchBarFrame
searchBarApply() _ = 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)
strongSelf.searchBarNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: baseWidth - 20.0, height: 36.0))
if !item.isEnabled { if !item.isEnabled {
if strongSelf.disabledOverlay == nil { if strongSelf.disabledOverlay == nil {
+8 -3
View File
@@ -3,9 +3,14 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library( swift_library(
name = "ChatListUI", name = "ChatListUI",
module_name = "ChatListUI", module_name = "ChatListUI",
srcs = glob([ srcs = glob(
"Sources/**/*.swift", [
]), "Sources/**/*.swift",
],
exclude = [
"Sources/ChatListFilterTabContainerNode.swift",
],
),
copts = [ copts = [
"-warnings-as-errors", "-warnings-as-errors",
], ],
@@ -6595,8 +6595,17 @@ private final class ChatListLocationContext {
var proxyButton: AnyComponentWithIdentity<NavigationButtonComponentEnvironment>? var proxyButton: AnyComponentWithIdentity<NavigationButtonComponentEnvironment>?
var storyButton: 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 rightButtons: [AnyComponentWithIdentity<NavigationButtonComponentEnvironment>] {
var result: [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 { if let rightButton = self.rightButton {
result.append(rightButton) result.append(rightButton)
} }
@@ -6631,6 +6640,90 @@ private final class ChatListLocationContext {
self.location = location self.location = location
self.parentController = parentController 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]) let hasProxy = context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.proxySettings])
|> map { sharedData -> (Bool, Bool) in |> map { sharedData -> (Bool, Bool) in
if let settings = sharedData.entries[SharedDataKeys.proxySettings]?.get(ProxySettings.self) { if let settings = sharedData.entries[SharedDataKeys.proxySettings]?.get(ProxySettings.self) {
@@ -6949,6 +7042,8 @@ private final class ChatListLocationContext {
deinit { deinit {
self.titleDisposable?.dispose() self.titleDisposable?.dispose()
self.stateDisposable?.dispose() self.stateDisposable?.dispose()
self.accountSwitcherDisposable?.dispose()
self.accountSwitcherAvatarDisposable?.dispose()
} }
private func updateChatList( private func updateChatList(
@@ -6982,6 +7077,7 @@ private final class ChatListLocationContext {
if case .chatList(.root) = self.location { if case .chatList(.root) = self.location {
self.rightButton = nil self.rightButton = nil
self.storyButton = nil self.storyButton = nil
self.accountSwitcherButton = nil
} }
let title = !stateAndFilterId.state.selectedPeerIds.isEmpty ? presentationData.strings.ChatList_SelectedChats(Int32(stateAndFilterId.state.selectedPeerIds.count)) : defaultTitle 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 { if case .chatList(.root) = self.location {
self.rightButton = nil self.rightButton = nil
self.storyButton = nil self.storyButton = nil
self.accountSwitcherButton = nil
} }
self.leftButton = AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent( self.leftButton = AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent(
content: .text(title: presentationData.strings.Common_Done, isBold: true), content: .text(title: presentationData.strings.Common_Done, isBold: true),
@@ -14,6 +14,7 @@ import MergedAvatarsNode
import TextNodeWithEntities import TextNodeWithEntities
import TextFormat import TextFormat
import AvatarNode import AvatarNode
import GlobalControlPanelsContext
class ChatListNoticeItem: ListViewItem { class ChatListNoticeItem: ListViewItem {
enum Action { enum Action {
@@ -25,12 +26,12 @@ class ChatListNoticeItem: ListViewItem {
let context: AccountContext let context: AccountContext
let theme: PresentationTheme let theme: PresentationTheme
let strings: PresentationStrings let strings: PresentationStrings
let notice: ChatListNotice let notice: GlobalControlPanelsContext.ChatListNotice
let action: (Action) -> Void let action: (Action) -> Void
let selectable: Bool = true 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.context = context
self.theme = theme self.theme = theme
self.strings = strings self.strings = strings
@@ -130,7 +131,7 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode {
self.arrowNode = ASImageNode() self.arrowNode = ASImageNode()
self.separatorNode = ASDisplayNode() 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.contentContainer.clipsToBounds = true
self.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 { public protocol NavigationButtonNode: ASDisplayNode {
func updateManualAlpha(alpha: CGFloat, transition: ContainedViewLayoutTransition) func updateManualAlpha(alpha: CGFloat, transition: ContainedViewLayoutTransition)
var mainContentNode: ASDisplayNode? { get } 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 nodes: [NavigationButtonItemNode] = []
private var disappearingNodes: [(frame: CGRect, size: CGSize, node: NavigationButtonItemNode)] = [] private var disappearingNodes: [(frame: CGRect, size: CGSize, node: NavigationButtonItemNode)] = []
@@ -1,12 +1,22 @@
import Foundation import Foundation
import UIKit import UIKit
import Display import Display
import AsyncDisplayKit
import SwiftSignalKit import SwiftSignalKit
import TelegramCore import TelegramCore
import Postbox import Postbox
import TelegramPresentationData import TelegramPresentationData
import ItemListUI import ItemListUI
import AccountContext 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 // MARK: - Entry Definition
@@ -17,6 +27,7 @@ private enum DeletedMessagesSection: Int32 {
private enum DeletedMessagesEntry: ItemListNodeEntry { private enum DeletedMessagesEntry: ItemListNodeEntry {
case enableToggle(PresentationTheme, String, Bool) case enableToggle(PresentationTheme, String, Bool)
case archiveMediaToggle(PresentationTheme, String, Bool) case archiveMediaToggle(PresentationTheme, String, Bool)
case transparencySlider(PresentationTheme, Int32, Bool)
case settingsInfo(PresentationTheme, String) case settingsInfo(PresentationTheme, String)
var section: ItemListSectionId { var section: ItemListSectionId {
@@ -29,8 +40,10 @@ private enum DeletedMessagesEntry: ItemListNodeEntry {
return 0 return 0
case .archiveMediaToggle: case .archiveMediaToggle:
return 1 return 1
case .settingsInfo: case .transparencySlider:
return 2 return 2
case .settingsInfo:
return 3
} }
} }
@@ -48,6 +61,12 @@ private enum DeletedMessagesEntry: ItemListNodeEntry {
return true return true
} }
return false 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): case let .settingsInfo(lhsTheme, lhsText):
if case let .settingsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { if case let .settingsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true return true
@@ -85,6 +104,16 @@ private enum DeletedMessagesEntry: ItemListNodeEntry {
arguments.toggleArchiveMedia(value) 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): case let .settingsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
} }
@@ -96,13 +125,16 @@ private enum DeletedMessagesEntry: ItemListNodeEntry {
private final class DeletedMessagesControllerArguments { private final class DeletedMessagesControllerArguments {
let toggleEnabled: (Bool) -> Void let toggleEnabled: (Bool) -> Void
let toggleArchiveMedia: (Bool) -> Void let toggleArchiveMedia: (Bool) -> Void
let updateTransparency: (Int32) -> Void
init( init(
toggleEnabled: @escaping (Bool) -> Void, toggleEnabled: @escaping (Bool) -> Void,
toggleArchiveMedia: @escaping (Bool) -> Void toggleArchiveMedia: @escaping (Bool) -> Void,
updateTransparency: @escaping (Int32) -> Void
) { ) {
self.toggleEnabled = toggleEnabled self.toggleEnabled = toggleEnabled
self.toggleArchiveMedia = toggleArchiveMedia self.toggleArchiveMedia = toggleArchiveMedia
self.updateTransparency = updateTransparency
} }
} }
@@ -111,10 +143,12 @@ private final class DeletedMessagesControllerArguments {
private struct DeletedMessagesControllerState: Equatable { private struct DeletedMessagesControllerState: Equatable {
var isEnabled: Bool var isEnabled: Bool
var archiveMedia: Bool var archiveMedia: Bool
var transparencyPercent: Int32
static func ==(lhs: DeletedMessagesControllerState, rhs: DeletedMessagesControllerState) -> Bool { static func ==(lhs: DeletedMessagesControllerState, rhs: DeletedMessagesControllerState) -> Bool {
return lhs.isEnabled == rhs.isEnabled && 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(.enableToggle(presentationData.theme, "Сохранять удалённые сообщения", state.isEnabled))
entries.append(.archiveMediaToggle(presentationData.theme, "Архивировать медиа", state.archiveMedia)) 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 return entries
} }
@@ -138,7 +173,8 @@ private func deletedMessagesControllerEntries(
public func deletedMessagesController(context: AccountContext) -> ViewController { public func deletedMessagesController(context: AccountContext) -> ViewController {
let initialState = DeletedMessagesControllerState( let initialState = DeletedMessagesControllerState(
isEnabled: AntiDeleteManager.shared.isEnabled, 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) let statePromise = ValuePromise(initialState, ignoreRepeated: true)
@@ -163,6 +199,15 @@ public func deletedMessagesController(context: AccountContext) -> ViewController
state.archiveMedia = value state.archiveMedia = value
return state 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) let controller = ItemListController(context: context, state: signal)
return controller 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 misc(PresentationTheme, String, String)
case deviceSpoof(PresentationTheme, String, String) case deviceSpoof(PresentationTheme, String, String)
case voiceMorpher(PresentationTheme, String, String) case voiceMorpher(PresentationTheme, String, String)
case sendDelay(PresentationTheme, String, String)
case info(PresentationTheme, String) case info(PresentationTheme, String)
var section: ItemListSectionId { var section: ItemListSectionId {
@@ -37,8 +38,10 @@ private enum GhostgramSettingsEntry: ItemListNodeEntry {
return 3 return 3
case .voiceMorpher: case .voiceMorpher:
return 4 return 4
case .info: case .sendDelay:
return 5 return 5
case .info:
return 6
} }
} }
@@ -74,6 +77,12 @@ private enum GhostgramSettingsEntry: ItemListNodeEntry {
return true return true
} }
return false 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): case let .info(lhsTheme, lhsText):
if case let .info(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { if case let .info(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true return true
@@ -144,6 +153,17 @@ private enum GhostgramSettingsEntry: ItemListNodeEntry {
arguments.openVoiceMorpher() 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): case let .info(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
} }
@@ -158,19 +178,22 @@ private final class GhostgramSettingsControllerArguments {
let openMisc: () -> Void let openMisc: () -> Void
let openDeviceSpoof: () -> Void let openDeviceSpoof: () -> Void
let openVoiceMorpher: () -> Void let openVoiceMorpher: () -> Void
let openSendDelay: () -> Void
init( init(
openDeletedMessages: @escaping () -> Void, openDeletedMessages: @escaping () -> Void,
openGhostMode: @escaping () -> Void, openGhostMode: @escaping () -> Void,
openMisc: @escaping () -> Void, openMisc: @escaping () -> Void,
openDeviceSpoof: @escaping () -> Void, openDeviceSpoof: @escaping () -> Void,
openVoiceMorpher: @escaping () -> Void openVoiceMorpher: @escaping () -> Void,
openSendDelay: @escaping () -> Void
) { ) {
self.openDeletedMessages = openDeletedMessages self.openDeletedMessages = openDeletedMessages
self.openGhostMode = openGhostMode self.openGhostMode = openGhostMode
self.openMisc = openMisc self.openMisc = openMisc
self.openDeviceSpoof = openDeviceSpoof self.openDeviceSpoof = openDeviceSpoof
self.openVoiceMorpher = openVoiceMorpher self.openVoiceMorpher = openVoiceMorpher
self.openSendDelay = openSendDelay
} }
} }
@@ -184,6 +207,8 @@ private struct GhostgramSettingsState: Equatable {
var miscActiveCount: Int var miscActiveCount: Int
var deviceSpoofEnabled: Bool var deviceSpoofEnabled: Bool
var voiceMorpherEnabled: Bool var voiceMorpherEnabled: Bool
var voiceMorpherPresetName: String
var sendDelayEnabled: Bool
static func current() -> GhostgramSettingsState { static func current() -> GhostgramSettingsState {
return GhostgramSettingsState( return GhostgramSettingsState(
@@ -193,7 +218,9 @@ private struct GhostgramSettingsState: Equatable {
miscEnabled: MiscSettingsManager.shared.isEnabled, miscEnabled: MiscSettingsManager.shared.isEnabled,
miscActiveCount: MiscSettingsManager.shared.activeFeatureCount, miscActiveCount: MiscSettingsManager.shared.activeFeatureCount,
deviceSpoofEnabled: DeviceSpoofManager.shared.isEnabled, 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)) entries.append(.deviceSpoof(presentationData.theme, "Подмена устройства", deviceSpoofStatus))
// Voice Morpher // Voice Morpher
let voiceMorpherStatus = state.voiceMorpherEnabled ? VoiceMorpherManager.shared.selectedPreset.name : "Выкл" let voiceMorpherStatus = state.voiceMorpherEnabled ? state.voiceMorpherPresetName : "Выкл"
entries.append(.voiceMorpher(presentationData.theme, "Голосовой двойник", voiceMorpherStatus)) entries.append(.voiceMorpher(presentationData.theme, "Голосовой двойник", voiceMorpherStatus))
// Send Delay
let sendDelayStatus = state.sendDelayEnabled ? "Вкл" : "Выкл"
entries.append(.sendDelay(presentationData.theme, "Отложка сообщений", sendDelayStatus))
// Info // Info
entries.append(.info(presentationData.theme, "Функции конфиденциальности Ghostgram. Скрытые отметки о прочтении, обход исчезающих сообщений, обход защиты от пересылки и другое.")) entries.append(.info(presentationData.theme, "Функции конфиденциальности Ghostgram. Скрытые отметки о прочтении, обход исчезающих сообщений, обход защиты от пересылки и другое."))
@@ -255,6 +286,9 @@ public func ghostgramSettingsController(context: AccountContext) -> ViewControll
}, },
openVoiceMorpher: { openVoiceMorpher: {
pushControllerImpl?(voiceMorpherController(context: context), true) pushControllerImpl?(voiceMorpherController(context: context), true)
},
openSendDelay: {
pushControllerImpl?(sendDelayController(context: context), true)
} }
) )
@@ -328,7 +328,7 @@ public func miscController(context: AccountContext) -> ViewController {
let controllerState = ItemListControllerState( let controllerState = ItemListControllerState(
presentationData: ItemListPresentationData(presentationData), presentationData: ItemListPresentationData(presentationData),
title: .text("Misc"), title: .text("Прочее"),
leftNavigationButton: nil, leftNavigationButton: nil,
rightNavigationButton: nil, rightNavigationButton: nil,
backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back),
@@ -90,7 +90,7 @@ class NotificationSearchItemNode: ListViewItemNode {
required init() { required init() {
self.searchBarNode = SearchBarPlaceholderNode() self.searchBarNode = SearchBarPlaceholderNode()
super.init(layerBacked: false, dynamicBounce: false) super.init(layerBacked: false)
self.addSubnode(self.searchBarNode) self.addSubnode(self.searchBarNode)
} }
@@ -104,16 +104,16 @@ class NotificationSearchItemNode: ListViewItemNode {
} }
func asyncLayout() -> (_ item: NotificationSearchItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) { func asyncLayout() -> (_ item: NotificationSearchItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let searchBarNodeLayout = self.searchBarNode.asyncLayout()
let placeholder = self.placeholder let placeholder = self.placeholder
return { item, params in return { item, params in
let baseWidth = params.width - params.leftInset - params.rightInset let baseWidth = params.width - params.leftInset - params.rightInset
let backgroundColor = item.theme.chatList.itemBackgroundColor 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 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()) let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 44.0), insets: UIEdgeInsets())
@@ -126,10 +126,9 @@ class NotificationSearchItemNode: ListViewItemNode {
transition = .immediate 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)) let searchBarSize = CGSize(width: baseWidth - 16.0, height: 28.0)
searchBarApply() 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)
strongSelf.searchBarNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: baseWidth - 16.0, height: 28.0))
transition.updateBackgroundColor(node: strongSelf, color: backgroundColor) 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 latePurple = UIColor(rgb: 0xaa56a6)
private let latePink = UIColor(rgb: 0xef476f) private let latePink = UIColor(rgb: 0xef476f)
private func textForTimeout(value: Int32) -> String { private func callStatusBarTextForTimeout(value: Int32) -> String {
if value < 3600 { if value < 3600 {
let minutes = value / 60 let minutes = value / 60
let seconds = 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 timerText = presentationData.strings.VoiceChat_StatusStartsIn(scheduledTimeIntervalString(strings: presentationData.strings, value: elapsedTime)).string
} else if elapsedTime < 0 { } else if elapsedTime < 0 {
isLate = true isLate = true
timerText = presentationData.strings.VoiceChat_StatusLateBy(textForTimeout(value: abs(elapsedTime))).string timerText = presentationData.strings.VoiceChat_StatusLateBy(callStatusBarTextForTimeout(value: abs(elapsedTime))).string
} else { } 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))) segments.append(.text(0, NSAttributedString(string: timerText, font: textFont, textColor: textColor)))
} else if let membersCount = membersCount { } else if let membersCount = membersCount {
@@ -13,7 +13,7 @@ private let pink = UIColor(rgb: 0xef436c)
private let latePurple = UIColor(rgb: 0x974aa9) private let latePurple = UIColor(rgb: 0x974aa9)
private let latePink = UIColor(rgb: 0xf0436c) private let latePink = UIColor(rgb: 0xf0436c)
private func textForTimeout(value: Int32) -> String { private func scheduledInfoTextForTimeout(value: Int32) -> String {
if value < 3600 { if value < 3600 {
let minutes = value / 60 let minutes = value / 60
let seconds = value % 60 let seconds = value % 60
@@ -150,7 +150,7 @@ final class VideoChatScheduledInfoComponent: Component {
if remainingSeconds >= 86400 { if remainingSeconds >= 86400 {
countdownText = scheduledTimeIntervalString(strings: component.strings, value: remainingSeconds) countdownText = scheduledTimeIntervalString(strings: component.strings, value: remainingSeconds)
} else { } else {
countdownText = textForTimeout(value: abs(remainingSeconds)) countdownText = scheduledInfoTextForTimeout(value: abs(remainingSeconds))
/*if remainingSeconds < 0 && !self.isLate { /*if remainingSeconds < 0 && !self.isLate {
self.isLate = true self.isLate = true
self.foregroundGradientLayer.colors = [latePink.cgColor, latePurple.cgColor, latePurple.cgColor] 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 latePurple = UIColor(rgb: 0x974aa9)
private let latePink = UIColor(rgb: 0xf0436c) private let latePink = UIColor(rgb: 0xf0436c)
private func textForTimeout(value: Int32) -> String { private func voiceChatTimerTextForTimeout(value: Int32) -> String {
if value < 3600 { if value < 3600 {
let minutes = value / 60 let minutes = value / 60
let seconds = value % 60 let seconds = value % 60
@@ -189,7 +189,7 @@ final class VoiceChatTimerNode: ASDisplayNode {
if elapsedTime >= 86400 { if elapsedTime >= 86400 {
timerText = scheduledTimeIntervalString(strings: self.strings, value: elapsedTime) timerText = scheduledTimeIntervalString(strings: self.strings, value: elapsedTime)
} else { } else {
timerText = textForTimeout(value: abs(elapsedTime)) timerText = voiceChatTimerTextForTimeout(value: abs(elapsedTime))
if elapsedTime < 0 && !self.isLate { if elapsedTime < 0 && !self.isLate {
self.isLate = true self.isLate = true
self.foregroundGradientLayer.colors = [latePink.cgColor, latePurple.cgColor, latePurple.cgColor] self.foregroundGradientLayer.colors = [latePink.cgColor, latePurple.cgColor, latePurple.cgColor]
@@ -11,6 +11,7 @@ public final class AntiDeleteManager {
private let defaults = UserDefaults.standard private let defaults = UserDefaults.standard
private let enabledKey = "antiDelete.enabled" private let enabledKey = "antiDelete.enabled"
private let archiveMediaKey = "antiDelete.archiveMedia" private let archiveMediaKey = "antiDelete.archiveMedia"
private let deletedMessageTransparencyKey = "antiDelete.deletedMessageTransparency"
private let archiveKey = "antiDelete.archive" private let archiveKey = "antiDelete.archive"
private let deletedIdsKey = "antiDelete.deletedIds" private let deletedIdsKey = "antiDelete.deletedIds"
@@ -26,6 +27,33 @@ public final class AntiDeleteManager {
set { defaults.set(newValue, forKey: archiveMediaKey) } 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 // MARK: - Deleted Message IDs Storage
private var deletedMessageIds: Set<String> = [] private var deletedMessageIds: Set<String> = []
@@ -120,6 +148,9 @@ public final class AntiDeleteManager {
if defaults.object(forKey: archiveMediaKey) == nil { if defaults.object(forKey: archiveMediaKey) == nil {
defaults.set(true, forKey: archiveMediaKey) defaults.set(true, forKey: archiveMediaKey)
} }
if defaults.object(forKey: deletedMessageTransparencyKey) == nil {
defaults.set(Self.defaultDeletedMessageTransparency, forKey: deletedMessageTransparencyKey)
}
loadArchive() loadArchive()
loadDeletedIds() loadDeletedIds()
} }
@@ -23,9 +23,6 @@ public final class GhostModeManager {
private let defaults = UserDefaults.standard private let defaults = UserDefaults.standard
// Prevents recursive mutual-exclusion calls
private var isApplyingMutualExclusion = false
// MARK: - Properties // MARK: - Properties
/// Master toggle for Ghost Mode. /// Master toggle for Ghost Mode.
@@ -34,11 +31,9 @@ public final class GhostModeManager {
get { defaults.bool(forKey: Keys.isEnabled) } get { defaults.bool(forKey: Keys.isEnabled) }
set { set {
defaults.set(newValue, forKey: Keys.isEnabled) defaults.set(newValue, forKey: Keys.isEnabled)
if newValue && !isApplyingMutualExclusion { if newValue {
// Ghost Mode ON disable Always Online // Ghost Mode ON disable Always Online so they don't coexist in UI
isApplyingMutualExclusion = true
MiscSettingsManager.shared.disableAlwaysOnlineForMutualExclusion() MiscSettingsManager.shared.disableAlwaysOnlineForMutualExclusion()
isApplyingMutualExclusion = false
} }
notifySettingsChanged() 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. /// 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 { public var shouldHideOnlineStatus: Bool {
guard isEnabled && hideOnlineStatus else { return false } guard isEnabled && hideOnlineStatus else { return false }
return !MiscSettingsManager.shared.shouldAlwaysBeOnline return !MiscSettingsManager.shared.alwaysOnline
} }
public var shouldHideTypingIndicator: Bool { 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. /// Force offline only when Ghost Mode is on AND Always Online is NOT active.
public var shouldForceOffline: Bool { public var shouldForceOffline: Bool {
guard isEnabled && forceOffline else { return false } guard isEnabled && forceOffline else { return false }
return !MiscSettingsManager.shared.shouldAlwaysBeOnline return !MiscSettingsManager.shared.alwaysOnline
} }
/// Count of active features (e.g., "5/5") /// Count of active features (e.g., "5/5")
@@ -131,17 +128,6 @@ public final class GhostModeManager {
/// Total number of features /// Total number of features
public static let totalFeatureCount = 5 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 // MARK: - Initialization
private init() { private init() {
@@ -16,9 +16,6 @@ public final class MiscSettingsManager {
private let defaults = UserDefaults.standard private let defaults = UserDefaults.standard
// Prevents recursive mutual-exclusion calls
private var isApplyingMutualExclusion = false
// MARK: - Main Toggle // MARK: - Main Toggle
public var isEnabled: Bool { public var isEnabled: Bool {
@@ -68,17 +65,12 @@ public final class MiscSettingsManager {
} }
/// Always appear as online. /// 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 { public var alwaysOnline: Bool {
get { defaults.bool(forKey: Keys.alwaysOnline) } get { defaults.bool(forKey: Keys.alwaysOnline) }
set { set {
defaults.set(newValue, forKey: Keys.alwaysOnline) defaults.set(newValue, forKey: Keys.alwaysOnline)
if newValue && !isApplyingMutualExclusion {
// Always Online ON disable Ghost Mode
isApplyingMutualExclusion = true
GhostModeManager.shared.disableForMutualExclusion()
isApplyingMutualExclusion = false
}
notifySettingsChanged() notifySettingsChanged()
} }
} }
@@ -122,7 +114,7 @@ public final class MiscSettingsManager {
disableViewOnceAutoDelete = true disableViewOnceAutoDelete = true
bypassScreenshotProtection = true bypassScreenshotProtection = true
blockAds = true blockAds = true
alwaysOnline = true // setter handles mutual exclusion alwaysOnline = true
} }
public func disableAll() { public func disableAll() {
@@ -136,12 +128,10 @@ public final class MiscSettingsManager {
// MARK: - Internal mutual exclusion (called by GhostModeManager) // MARK: - Internal mutual exclusion (called by GhostModeManager)
/// Called by GhostModeManager when Ghost Mode is turned on. /// 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() { public func disableAlwaysOnlineForMutualExclusion() {
isApplyingMutualExclusion = true
defaults.set(false, forKey: Keys.alwaysOnline) defaults.set(false, forKey: Keys.alwaysOnline)
notifySettingsChanged() notifySettingsChanged()
isApplyingMutualExclusion = false
} }
// MARK: - Notification // MARK: - Notification
@@ -365,14 +365,46 @@ public func enqueueMessages(account: Account, peerId: PeerId, messages: [Enqueue
} else { } else {
signal = .single(messages.map { (false, $0) }) 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 return signal
|> mapToSignal { messages -> Signal<[MessageId?], NoError> in |> mapToSignal { messages -> Signal<[MessageId?], NoError> in
return account.postbox.transaction { transaction -> [MessageId?] 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> { public func enqueueMessagesToMultiplePeers(account: Account, peerIds: [PeerId], threadIds: [PeerId: Int64], messages: [EnqueueMessage]) -> Signal<[MessageId], NoError> {
let signal: Signal<[(Bool, EnqueueMessage)], NoError> let signal: Signal<[(Bool, EnqueueMessage)], NoError>
if let transformOutgoingMessageMedia = account.transformOutgoingMessageMedia { 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 { if AntiDeleteManager.shared.isEnabled {
let messageIds = transaction.messageIdsForGlobalIds(ids) let messageIds = transaction.messageIdsForGlobalIds(ids)
for (index, messageId) in messageIds.enumerated() { 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) { if let message = transaction.getMessage(messageId) {
let globalId = index < ids.count ? ids[index] : 0 let globalId = index < ids.count ? ids[index] : 0
@@ -4289,6 +4292,9 @@ func replayFinalState(
if AntiDeleteManager.shared.isEnabled { if AntiDeleteManager.shared.isEnabled {
let messageIds = transaction.messageIdsForGlobalIds(ids) let messageIds = transaction.messageIdsForGlobalIds(ids)
for messageId in messageIds { 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 // Mark as deleted for icon display
AntiDeleteManager.shared.markAsDeleted(peerId: messageId.peerId.toInt64(), messageId: messageId.id) 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 // ANTI-DELETE: Archive channel messages with full content before deletion
if AntiDeleteManager.shared.isEnabled { if AntiDeleteManager.shared.isEnabled {
for messageId in ids { 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) { if let message = transaction.getMessage(messageId) {
// Extract text content // Extract text content
let textContent = message.text let textContent = message.text
@@ -4370,6 +4379,9 @@ func replayFinalState(
// ANTI-DELETE: Mark messages as deleted instead of removing them // ANTI-DELETE: Mark messages as deleted instead of removing them
if AntiDeleteManager.shared.isEnabled { if AntiDeleteManager.shared.isEnabled {
for messageId in ids { 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 // Mark as deleted for icon display
AntiDeleteManager.shared.markAsDeleted(peerId: messageId.peerId.toInt64(), messageId: messageId.id) 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) /// 2. Ghost Mode hide online status skip update entirely (freeze last-seen)
/// 3. Default app behaviour (wasOnline) /// 3. Default app behaviour (wasOnline)
private func refreshPresence() { 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 let ghostHideOnline = GhostModeManager.shared.shouldHideOnlineStatus
if alwaysOnline { if alwaysOnline {
// Always Online wins push online regardless of Ghost Mode // Always Online wins push online regardless of Ghost Mode
sendPresenceUpdate(online: true) sendPresenceUpdate(online: true)
} else if ghostHideOnline { } else if ghostHideOnline {
// Ghost Mode active, no Always Online freeze presence (don't send anything) // Ghost Mode active: actively send offline so the server immediately
self.onlineTimer?.invalidate() // hides our last-seen instead of keeping the stale "online" status.
self.onlineTimer = nil sendPresenceUpdate(online: false)
} else { } else {
// Normal mode follow the app-level state // Normal mode follow the app-level state
sendPresenceUpdate(online: wasOnline) sendPresenceUpdate(online: wasOnline)
@@ -27,7 +27,6 @@ import MediaEditor
import AvatarBackground import AvatarBackground
import LottieComponent import LottieComponent
import UndoUI import UndoUI
import PremiumAlertController
public struct AvatarKeyboardInputData: Equatable { public struct AvatarKeyboardInputData: Equatable {
var emoji: EmojiPagerContentComponent 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 ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode {
public class ContentContainer { public class ContentContainer {
public let contentMessageStableId: UInt32 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 var incomingOffset: CGFloat = 0.0
switch backgroundType { switch backgroundType {
case .incoming: 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) 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 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 { if hasSelection {
var currentMaskView: UIImageView? var currentMaskView: UIImageView?
if let maskView = strongSelf.contentContainersWrapperNode.view.mask as? UIImageView { if let maskView = strongSelf.contentContainersWrapperNode.view.mask as? UIImageView {
@@ -29,6 +29,27 @@ public final class NavigationButtonComponent: Component {
case more case more
case icon(imageName: String) case icon(imageName: String)
case proxy(status: ChatTitleProxyStatus) 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 public let content: Content
@@ -62,6 +83,12 @@ public final class NavigationButtonComponent: Component {
private var moreButton: MoreHeaderButton? 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 component: NavigationButtonComponent?
private var theme: PresentationTheme? private var theme: PresentationTheme?
@@ -74,19 +101,23 @@ public final class NavigationButtonComponent: Component {
guard let self else { guard let self else {
return return
} }
if highlighted { let alpha: CGFloat = highlighted ? 0.55 : 1.0
self.textView?.alpha = 0.6 self.textView?.alpha = alpha
self.proxyNode?.alpha = 0.6 self.proxyNode?.alpha = alpha
self.iconView?.alpha = 0.6 self.iconView?.alpha = alpha
} else { self.avatarContainerView?.alpha = alpha
self.textView?.alpha = 1.0 if !highlighted {
self.textView?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) let animateAlpha = { (layer: CALayer?) in
let anim = CABasicAnimation(keyPath: "opacity")
self.proxyNode?.alpha = 1.0 anim.fromValue = 0.55
self.proxyNode?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) anim.toValue = 1.0
anim.duration = 0.2
self.iconView?.alpha = 1.0 layer?.add(anim, forKey: "opacity")
self.iconView?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) }
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) 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 { func update(component: NavigationButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<NavigationButtonComponentEnvironment>, transition: ComponentTransition) -> CGSize {
self.component = component self.component = component
@@ -113,6 +189,7 @@ public final class NavigationButtonComponent: Component {
var imageName: String? var imageName: String?
var proxyStatus: ChatTitleProxyStatus? var proxyStatus: ChatTitleProxyStatus?
var isMore: Bool = false var isMore: Bool = false
var avatarContent: (peerId: String, image: UIImage?)? = nil
switch component.content { switch component.content {
case let .text(title, isBold): case let .text(title, isBold):
@@ -123,10 +200,13 @@ public final class NavigationButtonComponent: Component {
imageName = imageNameValue imageName = imageNameValue
case let .proxy(status): case let .proxy(status):
proxyStatus = status proxyStatus = status
case let .avatar(peerId, image):
avatarContent = (peerId, image)
} }
var size = CGSize(width: 0.0, height: availableSize.height) var size = CGSize(width: 0.0, height: availableSize.height)
// MARK: Text
if let textString = textString { if let textString = textString {
let textView: ImmediateTextView let textView: ImmediateTextView
if let current = self.textView { if let current = self.textView {
@@ -144,11 +224,13 @@ public final class NavigationButtonComponent: Component {
size.width = max(44.0, textSize.width + textInset * 2.0) 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) 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 { } else if let textView = self.textView {
self.textView = nil self.textView = nil
textView.removeFromSuperview() textView.removeFromSuperview()
} }
// MARK: Icon
if let imageName = imageName { if let imageName = imageName {
let iconView: UIImageView let iconView: UIImageView
if let current = self.iconView { if let current = self.iconView {
@@ -166,15 +248,16 @@ public final class NavigationButtonComponent: Component {
if let iconSize = iconView.image?.size { if let iconSize = iconView.image?.size {
size.width = 44.0 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) 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 { } else if let iconView = self.iconView {
self.iconView = nil self.iconView = nil
iconView.removeFromSuperview() iconView.removeFromSuperview()
self.iconImageName = nil self.iconImageName = nil
} }
// MARK: Proxy
if let proxyStatus = proxyStatus { if let proxyStatus = proxyStatus {
let proxyNode: ChatTitleProxyNode let proxyNode: ChatTitleProxyNode
if let current = self.proxyNode { if let current = self.proxyNode {
@@ -191,13 +274,14 @@ public final class NavigationButtonComponent: Component {
proxyNode.theme = theme proxyNode.theme = theme
proxyNode.status = proxyStatus 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) 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 { } else if let proxyNode = self.proxyNode {
self.proxyNode = nil self.proxyNode = nil
proxyNode.removeFromSupernode() proxyNode.removeFromSupernode()
} }
// MARK: More
if isMore { if isMore {
let moreButton: MoreHeaderButton let moreButton: MoreHeaderButton
if let current = self.moreButton, !themeUpdated { if let current = self.moreButton, !themeUpdated {
@@ -233,13 +317,52 @@ public final class NavigationButtonComponent: Component {
size.width = 44.0 size.width = 44.0
moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: theme.rootController.navigationBar.buttonColor))) 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) 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 { } else if let moreButton = self.moreButton {
self.moreButton = nil self.moreButton = nil
moreButton.removeFromSupernode() 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 return size
} }
} }
@@ -5,6 +5,8 @@ swift_library(
module_name = "GiftViewScreen", module_name = "GiftViewScreen",
srcs = glob([ srcs = glob([
"Sources/**/*.swift", "Sources/**/*.swift",
], exclude = [
"Sources/TableComponent.swift",
]), ]),
copts = [ copts = [
"-warnings-as-errors", "-warnings-as-errors",
@@ -133,7 +133,7 @@ public func giftOfferAlertController(
HStack(items, spacing: 4.0) HStack(items, spacing: 4.0)
) )
tableItems.append(.init( tableItems.append(TableComponent.Item(
id: id, id: id,
title: title, title: title,
hasBackground: false, hasBackground: false,
@@ -180,12 +180,12 @@ public func giftOfferAlertController(
AlertTextComponent(content: .plain(text)) AlertTextComponent(content: .plain(text))
) )
)) ))
content.append(AnyComponentWithIdentity( let tableComponent = AnyComponent(AlertTableComponent(items: tableItems))
let tableEntry = AnyComponentWithIdentity<AlertComponentEnvironment>(
id: "table", id: "table",
component: AnyComponent( component: tableComponent
AlertTableComponent(items: tableItems) )
) content.append(tableEntry)
))
if let valueAmount = gift.valueUsdAmount { if let valueAmount = gift.valueUsdAmount {
let resaleConfiguration = StarsSubscriptionConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let resaleConfiguration = StarsSubscriptionConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
@@ -118,7 +118,7 @@ public func giftTransferAlertController(
HStack(items, spacing: 4.0) HStack(items, spacing: 4.0)
) )
tableItems.append(.init( tableItems.append(TableComponent.Item(
id: id, id: id,
title: title, title: title,
hasBackground: false, hasBackground: false,
@@ -165,12 +165,12 @@ public func giftTransferAlertController(
AlertTextComponent(content: .plain(text)) AlertTextComponent(content: .plain(text))
) )
)) ))
content.append(AnyComponentWithIdentity( let tableComponent = AnyComponent(AlertTableComponent(items: tableItems))
let tableEntry = AnyComponentWithIdentity<AlertComponentEnvironment>(
id: "table", id: "table",
component: AnyComponent( component: tableComponent
AlertTableComponent(items: tableItems) )
) content.append(tableEntry)
))
let alertController = ChatMessagePaymentAlertController( let alertController = ChatMessagePaymentAlertController(
context: context, context: context,
@@ -8,7 +8,7 @@ import ChatPresentationInterfaceState
import AsyncDisplayKit import AsyncDisplayKit
import AccountContext import AccountContext
open class ChatTitleAccessoryPanelNode: ASDisplayNode { open class LegacyChatTitleAccessoryPanelNode: ASDisplayNode {
public typealias LayoutResult = ChatControllerCustomNavigationPanelNodeLayoutResult public typealias LayoutResult = ChatControllerCustomNavigationPanelNodeLayoutResult
open var interfaceInteraction: ChatPanelInterfaceInteraction? open var interfaceInteraction: ChatPanelInterfaceInteraction?
@@ -19,11 +19,11 @@ open class ChatTitleAccessoryPanelNode: ASDisplayNode {
} }
public final class LegacyChatHeaderPanelComponent: Component { public final class LegacyChatHeaderPanelComponent: Component {
public let panelNode: ChatTitleAccessoryPanelNode public let panelNode: LegacyChatTitleAccessoryPanelNode
public let interfaceState: ChatPresentationInterfaceState public let interfaceState: ChatPresentationInterfaceState
public init( public init(
panelNode: ChatTitleAccessoryPanelNode, panelNode: LegacyChatTitleAccessoryPanelNode,
interfaceState: ChatPresentationInterfaceState interfaceState: ChatPresentationInterfaceState
) { ) {
self.panelNode = panelNode self.panelNode = panelNode
@@ -232,20 +232,34 @@ final class AffiliateProgramSetupScreenComponent: Component {
) )
)) ))
let tableItems: [TableComponent.Item] = [ let textColor = environment.theme.actionSheet.primaryTextColor
TableComponent.Item(id: 0, title: environment.strings.AffiliateSetup_AlertApply_SectionCommission, component: AnyComponent(MultilineTextComponent( let commissionItemText = NSAttributedString(
text: .plain(NSAttributedString(string: commissionTitle, font: Font.regular(15.0), textColor: environment.theme.actionSheet.primaryTextColor)) string: commissionTitle,
))), font: Font.regular(15.0),
TableComponent.Item(id: 1, title: environment.strings.AffiliateSetup_AlertApply_SectionDuration, component: AnyComponent(MultilineTextComponent( textColor: textColor
text: .plain(NSAttributedString(string: durationTitle, font: Font.regular(15.0), textColor: environment.theme.actionSheet.primaryTextColor)) )
))) let durationItemText = NSAttributedString(
] string: durationTitle,
content.append(AnyComponentWithIdentity( 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", id: "table",
component: AnyComponent( component: tableComponent
AlertTableComponent(items: tableItems) )
) content.append(tableEntry)
))
let alertController = AlertScreen( let alertController = AlertScreen(
context: component.context, context: component.context,
@@ -6,7 +6,7 @@ import TelegramPresentationData
import MultilineTextComponent import MultilineTextComponent
import AlertComponent import AlertComponent
final class TableComponent: CombinedComponent { final class AffiliateTableComponent: CombinedComponent {
class Item: Equatable { class Item: Equatable {
public let id: AnyHashable public let id: AnyHashable
public let title: String public let title: String
@@ -45,7 +45,7 @@ final class TableComponent: CombinedComponent {
self.items = items self.items = items
} }
public static func ==(lhs: TableComponent, rhs: TableComponent) -> Bool { public static func ==(lhs: AffiliateTableComponent, rhs: AffiliateTableComponent) -> Bool {
if lhs.theme !== rhs.theme { if lhs.theme !== rhs.theme {
return false return false
} }
@@ -228,9 +228,9 @@ private final class TableAlertContentComponent: CombinedComponent {
let theme: PresentationTheme let theme: PresentationTheme
let title: String let title: String
let text: 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.theme = theme
self.title = title self.title = title
self.text = text self.text = text
@@ -256,7 +256,7 @@ private final class TableAlertContentComponent: CombinedComponent {
public static var body: Body { public static var body: Body {
let title = Child(MultilineTextComponent.self) let title = Child(MultilineTextComponent.self)
let text = Child(MultilineTextComponent.self) let text = Child(MultilineTextComponent.self)
let table = Child(TableComponent.self) let table = Child(AffiliateTableComponent.self)
return { context in return { context in
let title = title.update( 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 ProgressNavigationButtonNode
import AccountContext import AccountContext
import SearchUI import SearchUI
import ChatListUI import func ChatListUI.chatListFilterItems
import enum ChatListUI.ChatListContainerNodeFilter
import CounterControllerTitleView import CounterControllerTitleView
import ChatListFilterTabContainerNode import ChatListFilterTabContainerNode
@@ -69,7 +69,7 @@ public final class BirthdayPickerComponent: Component {
private let calendar = Calendar(identifier: .gregorian) private let calendar = Calendar(identifier: .gregorian)
private var value = TelegramBirthday(day: 1, month: 1, year: nil) private var value = TelegramBirthday(day: 1, month: 1, year: nil)
private var minYear: Int32 = 1900 private var minYear: Int32 = 1
private let maxYear: Int32 private let maxYear: Int32
override init(frame: CGRect) { 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) 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 { if let currentItem = self.globalControlPanelsContext?.tempVoicePlaylistCurrentItem {
self.chatDisplayNode.historyNode.voicePlaylistItemChanged(nil, currentItem) self.chatDisplayNode.historyNode.voicePlaylistItemChanged(nil, currentItem)
} }
@@ -146,7 +146,6 @@ import ChatTextInputPanelNode
import ChatInputAccessoryPanel import ChatInputAccessoryPanel
import GlobalControlPanelsContext import GlobalControlPanelsContext
import ChatSearchNavigationContentNode import ChatSearchNavigationContentNode
import ChatAgeRestrictionAlertController
public final class ChatControllerOverlayPresentationData { public final class ChatControllerOverlayPresentationData {
public let expandData: (ASDisplayNode?, () -> Void) public let expandData: (ASDisplayNode?, () -> Void)
@@ -830,7 +829,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return true 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 { guard let self, self.isNodeLoaded, let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(message.id) else {
return false return false
} }
@@ -1566,7 +1565,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.controllerInteraction?.isOpeningMediaSignal = openChatMessageParams.blockInteraction.get() self.controllerInteraction?.isOpeningMediaSignal = openChatMessageParams.blockInteraction.get()
return context.sharedContext.openChatMessage(openChatMessageParams) 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 var expandAvatar = false
if case let .groupParticipant(storyStats, avatarHeaderNode) = source { if case let .groupParticipant(storyStats, avatarHeaderNode) = source {
if let storyStats, storyStats.totalCount != 0, let avatarHeaderNode = avatarHeaderNode as? ChatMessageAvatarHeaderNodeImpl { 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 fromReactionMessageId = fromMessage?.id
} }
self?.openPeer(peer: peer, navigation: navigation, fromMessage: fromMessage, fromReactionMessageId: fromReactionMessageId, expandAvatar: expandAvatar) 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) 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 { guard let self, self.isNodeLoaded else {
return return
} }
self.openMessageContextMenu(message: message, selectAll: selectAll, node: node, frame: frame, anyRecognizer: anyRecognizer, location: location) 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 { guard let self else {
return return
} }
self.openMessageReactionContextMenu(message: message, sourceView: sourceView, gesture: gesture, value: value) 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 { guard let strongSelf = self else {
return 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 { guard let strongSelf = self else {
return return
} }
@@ -893,7 +893,8 @@ extension ChatControllerImpl {
peerVerification = cachedChannelData.verification 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 let cachedGroupData = peerView.cachedData as? CachedGroupData {
if !cachedGroupData.botInfos.isEmpty { if !cachedGroupData.botInfos.isEmpty {
hasBots = true hasBots = true
@@ -1371,7 +1372,8 @@ extension ChatControllerImpl {
var alwaysShowGiftButton = false var alwaysShowGiftButton = false
var disallowedGifts: TelegramDisallowedGifts? var disallowedGifts: TelegramDisallowedGifts?
if let peer = peerView.peers[peerView.peerId] { 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 { if let cachedData = peerView.cachedData as? CachedUserData {
contactStatus = ChatContactStatus(canAddContact: !peerView.peerIsContact, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: nil, managingBot: managingBot) contactStatus = ChatContactStatus(canAddContact: !peerView.peerIsContact, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: nil, managingBot: managingBot)
if case let .known(value) = cachedData.businessIntro { 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 { if self.historyNodeContainer.isSecret != isSecret {
self.historyNodeContainer.isSecret = isSecret self.historyNodeContainer.isSecret = isSecret
setLayerDisableScreenshots(self.titleAccessoryPanelContainer.layer, isSecret) setLayerDisableScreenshots(self.titleAccessoryPanelContainer.layer, isSecret)
@@ -4600,12 +4602,10 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
} }
} }
self.setupSendActionOnViewUpdate({ [weak self] in // GHOSTGRAM: When send delay is active, scheduled messages
guard let self, let textInputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode else { // don't trigger history view update, so clear input immediately.
return if SendDelayManager.shared.isEnabled {
}
self.collapseInput() self.collapseInput()
self.ignoreUpdateHeight = true self.ignoreUpdateHeight = true
textInputPanelNode.text = "" textInputPanelNode.text = ""
self.requestUpdateChatInterfaceState(.immediate, overrideThreadId == nil, { state in self.requestUpdateChatInterfaceState(.immediate, overrideThreadId == nil, { state in
@@ -4624,7 +4624,33 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
return state return state
}) })
self.ignoreUpdateHeight = false 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() completion()
self.sendMessages(messages, silentPosting, scheduleTime, repeatPeriod, messages.count > 1, postpone) 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( let alwaysDisplayTranscribeButton = ChatMessageItemAssociatedData.DisplayTranscribeButton(
canBeDisplayed: suggestAudioTranscription.0 < 2, canBeDisplayed: suggestAudioTranscription.0 < 2,
displayForNotConsumed: suggestAudioTranscription.1, displayForNotConsumed: suggestAudioTranscription.1,
@@ -2102,7 +2106,8 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
selectedMessages: selectedMessages, selectedMessages: selectedMessages,
presentationData: chatPresentationData, presentationData: chatPresentationData,
historyAppearsCleared: historyAppearsCleared, historyAppearsCleared: historyAppearsCleared,
skipViewOnceMedia: mode != .bubbles, // GHOSTGRAM: Keep view-once media visible if bypass is enabled
skipViewOnceMedia: MiscSettingsManager.shared.shouldDisableViewOnceAutoDelete ? false : (mode != .bubbles),
pendingUnpinnedAllMessages: pendingUnpinnedAllMessages, pendingUnpinnedAllMessages: pendingUnpinnedAllMessages,
pendingRemovedMessages: pendingRemovedMessages, pendingRemovedMessages: pendingRemovedMessages,
associatedData: associatedData, associatedData: associatedData,
@@ -2110,8 +2115,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
customChannelDiscussionReadState: customChannelDiscussionReadState, customChannelDiscussionReadState: customChannelDiscussionReadState,
customThreadOutgoingReadState: customThreadOutgoingReadState, customThreadOutgoingReadState: customThreadOutgoingReadState,
cachedData: data.cachedData, cachedData: data.cachedData,
adMessage: allAdMessages.fixed, // GHOSTGRAM: Block ads if enabled in Misc settings
dynamicAdMessages: allAdMessages.opportunistic 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 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) 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.chatLocation = chatLocation
self.interaction = interaction 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 let placeholderText: String
switch chatLocation { switch chatLocation {
case .peer, .replyThread, .customChatContents: case .peer, .replyThread, .customChatContents:
@@ -90,10 +90,11 @@ final class ChatSearchNavigationContentNode: NavigationBarContentNode {
return 54.0 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)) 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.frame = searchBarFrame
self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: leftInset, rightInset: rightInset, transition: transition) self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: leftInset, rightInset: rightInset, transition: transition)
return size
} }
func activate() { func activate() {
@@ -106,7 +107,7 @@ final class ChatSearchNavigationContentNode: NavigationBarContentNode {
func update(presentationInterfaceState: ChatPresentationInterfaceState) { func update(presentationInterfaceState: ChatPresentationInterfaceState) {
if let search = presentationInterfaceState.search { 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 { switch search.domain {
case .everything, .tag: case .everything, .tag:
@@ -1,16 +1,4 @@
import Foundation import class LegacyChatHeaderPanelComponent.LegacyChatTitleAccessoryPanelNode
import UIKit
import Display
import AsyncDisplayKit
import ChatPresentationInterfaceState
import AccountContext
class ChatTitleAccessoryPanelNode: ASDisplayNode { class ChatTitleAccessoryPanelNode: LegacyChatTitleAccessoryPanelNode {
typealias LayoutResult = ChatControllerCustomNavigationPanelNode.LayoutResult
var interfaceInteraction: ChatPanelInterfaceInteraction?
func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> LayoutResult {
preconditionFailure()
}
} }
+2 -1
View File
@@ -49,6 +49,8 @@ genrule(
BUILD_ARCH="arm64" BUILD_ARCH="arm64"
elif [ "$(TARGET_CPU)" == "ios_sim_arm64" ]; then elif [ "$(TARGET_CPU)" == "ios_sim_arm64" ]; then
BUILD_ARCH="sim_arm64" BUILD_ARCH="sim_arm64"
elif [ "$(TARGET_CPU)" == "ios_x86_64" ]; then
BUILD_ARCH="sim_x86_64"
else else
echo "Unsupported architecture $(TARGET_CPU)" echo "Unsupported architecture $(TARGET_CPU)"
fi fi
@@ -119,4 +121,3 @@ objc_library(
"//visibility:public", "//visibility:public",
], ],
) )
+7 -1
View File
@@ -18,6 +18,13 @@ elif [ "$ARCH" = "sim_arm64" ]; then
custom_xcode_path="$(xcode-select -p)/" custom_xcode_path="$(xcode-select -p)/"
sed -i '' "s|/Applications/Xcode.app/Contents/Developer/|$custom_xcode_path|g" "$TARGET_CROSSFILE" sed -i '' "s|/Applications/Xcode.app/Contents/Developer/|$custom_xcode_path|g" "$TARGET_CROSSFILE"
CROSSFILE="../package/crossfiles/arm64-iPhoneSimulator-custom.meson" 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 else
echo "Unsupported architecture $ARCH" echo "Unsupported architecture $ARCH"
exit 1 exit 1
@@ -33,4 +40,3 @@ ninja
popd popd
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_SYSTEM_PROCESSOR aarch64)" >> toolchain.cmake
echo "set(CMAKE_C_COMPILER $(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang)" >> 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 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 make
else else
+8 -1
View File
@@ -38,8 +38,15 @@ genrule(
BUILD_ARCH="arm64" BUILD_ARCH="arm64"
elif [ "$(TARGET_CPU)" == "ios_sim_arm64" ]; then elif [ "$(TARGET_CPU)" == "ios_sim_arm64" ]; then
BUILD_ARCH="sim_arm64" 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 else
echo "Unsupported architecture $(TARGET_CPU)" echo "Unsupported architecture $(TARGET_CPU)" >&2
exit 1
fi fi
BUILD_DIR="$(RULEDIR)/build_$${BUILD_ARCH}" 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 -DCMAKE_BUILD_TYPE=Release"
options="$options -DIOS_DEPLOYMENT_TARGET=13.0" 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" cd "$BUILD_DIR"
# Generate source files # Generate source files
mkdir native-build mkdir native-build
cd native-build cd native-build
cmake -DTD_GENERATE_SOURCE_FILES=ON ../td cmake -DTD_GENERATE_SOURCE_FILES=ON ../td
cmake --build . -- -j$(sysctl -n hw.ncpu) cmake --build . -- -j"$BUILD_JOBS"
cd .. cd ..
if [ "$ARCH" = "arm64" ]; then if [ "$ARCH" = "arm64" ]; then
IOS_PLATFORMDIR="$(xcode-select -p)/Platforms/iPhoneOS.platform" IOS_PLATFORMDIR="$(xcode-select -p)/Platforms/iPhoneOS.platform"
IOS_SYSROOT=($IOS_PLATFORMDIR/Developer/SDKs/iPhoneOS*.sdk) 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 elif [ "$ARCH" = "sim_arm64" ]; then
IOS_PLATFORMDIR="$(xcode-select -p)/Platforms/iPhoneSimulator.platform" IOS_PLATFORMDIR="$(xcode-select -p)/Platforms/iPhoneSimulator.platform"
IOS_SYSROOT=($IOS_PLATFORMDIR/Developer/SDKs/iPhoneSimulator*.sdk) 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 else
echo "Unsupported architecture $ARCH" echo "Unsupported architecture $ARCH"
exit 1 exit 1
fi fi
export CFLAGS="-arch ${cmake_arch} --target=${clang_target} ${minimum_target_flag}"
export CXXFLAGS="$CFLAGS"
export LDFLAGS="$CFLAGS"
# Common build steps # Common build steps
mkdir build mkdir build
cd build cd build
touch toolchain.cmake touch toolchain.cmake
echo "set(CMAKE_SYSTEM_NAME Darwin)" >> 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_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 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"