feat: новые функции, исправлены критические ошибки сборки и баги интерфейса, больше подписей в файлах
@@ -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/
|
||||||
|
|||||||
@@ -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
|
||||||
],
|
],
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 665 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 623 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
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);
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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']
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||