feat: новые функции, исправлены критические ошибки сборки и баги интерфейса, больше подписей в файлах
@@ -84,3 +84,5 @@ xcode-files
|
||||
/codesigning/
|
||||
/build-system/real-codesigning/
|
||||
/build-system/local-codesigning/
|
||||
build-output/
|
||||
build-output/
|
||||
|
||||
@@ -36,6 +36,7 @@ load(
|
||||
"telegram_bazel_path",
|
||||
"telegram_use_xcode_managed_codesigning",
|
||||
"telegram_bundle_id",
|
||||
"telegram_is_appstore_build",
|
||||
"telegram_aps_environment",
|
||||
"telegram_team_id",
|
||||
"telegram_enable_icloud",
|
||||
@@ -509,6 +510,16 @@ aps_fragment = "" if telegram_aps_environment == "" else """
|
||||
<string>{telegram_aps_environment}</string>
|
||||
""".format(telegram_aps_environment=telegram_aps_environment)
|
||||
|
||||
beta_reports_active_fragment = "" if telegram_is_appstore_build != "true" else """
|
||||
<key>beta-reports-active</key>
|
||||
<true/>
|
||||
"""
|
||||
|
||||
get_task_allow_fragment = """
|
||||
<key>get-task-allow</key>
|
||||
<{value}/>
|
||||
""".format(value = "false" if telegram_is_appstore_build == "true" else "true")
|
||||
|
||||
app_groups_fragment = """
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
@@ -546,6 +557,8 @@ plist_fragment(
|
||||
extension = "entitlements",
|
||||
template = "".join([
|
||||
aps_fragment,
|
||||
beta_reports_active_fragment,
|
||||
get_task_allow_fragment,
|
||||
app_groups_fragment,
|
||||
siri_fragment,
|
||||
associated_domains_fragment,
|
||||
@@ -1718,7 +1731,7 @@ ios_application(
|
||||
":RequiredDeviceCapabilitiesPlist",
|
||||
":UrlTypesInfoPlist",
|
||||
],
|
||||
app_icons = [ ":{}_icon".format(name) for name in composer_icon_folders ],
|
||||
app_icons = [":DefaultAppIcon"],
|
||||
alternate_icons = [
|
||||
":{}".format(name) for name in alternate_icon_folders
|
||||
],
|
||||
|
||||
|
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",
|
||||
"idiom": "iphone",
|
||||
"scale": "2x",
|
||||
"size": "20x20"
|
||||
"filename" : "GhostIcon@40x40.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename": "GhostIcon@60x60.png",
|
||||
"idiom": "iphone",
|
||||
"scale": "3x",
|
||||
"size": "20x20"
|
||||
"filename" : "GhostIcon@60x60.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename": "GhostIcon@58x58.png",
|
||||
"idiom": "iphone",
|
||||
"scale": "2x",
|
||||
"size": "29x29"
|
||||
"filename" : "GhostIcon@58x58.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename": "GhostIcon@87x87.png",
|
||||
"idiom": "iphone",
|
||||
"scale": "3x",
|
||||
"size": "29x29"
|
||||
"filename" : "GhostIcon@87x87.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename": "GhostIcon@80x80.png",
|
||||
"idiom": "iphone",
|
||||
"scale": "2x",
|
||||
"size": "40x40"
|
||||
"filename" : "GhostIcon@80x80.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename": "GhostIcon@120x120.png",
|
||||
"idiom": "iphone",
|
||||
"scale": "3x",
|
||||
"size": "40x40"
|
||||
"filename" : "GhostIcon@120x120.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename": "GhostIcon@120x120.png",
|
||||
"idiom": "iphone",
|
||||
"scale": "2x",
|
||||
"size": "60x60"
|
||||
"filename" : "GhostIcon@120x120.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename": "GhostIcon@180x180.png",
|
||||
"idiom": "iphone",
|
||||
"scale": "3x",
|
||||
"size": "60x60"
|
||||
"filename" : "GhostIcon@180x180.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename": "GhostIcon@20x20.png",
|
||||
"idiom": "ipad",
|
||||
"scale": "1x",
|
||||
"size": "20x20"
|
||||
"filename" : "GhostIcon@40x40.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename": "GhostIcon@40x40.png",
|
||||
"idiom": "ipad",
|
||||
"scale": "2x",
|
||||
"size": "20x20"
|
||||
"filename" : "GhostIcon@58x58.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename": "GhostIcon@29x29.png",
|
||||
"idiom": "ipad",
|
||||
"scale": "1x",
|
||||
"size": "29x29"
|
||||
"filename" : "GhostIcon@80x80.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename": "GhostIcon@58x58.png",
|
||||
"idiom": "ipad",
|
||||
"scale": "2x",
|
||||
"size": "29x29"
|
||||
"filename" : "GhostIcon@152x152.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename": "GhostIcon@40x40.png",
|
||||
"idiom": "ipad",
|
||||
"scale": "1x",
|
||||
"size": "40x40"
|
||||
"filename" : "GhostIcon@167x167.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"filename": "GhostIcon@80x80.png",
|
||||
"idiom": "ipad",
|
||||
"scale": "2x",
|
||||
"size": "40x40"
|
||||
},
|
||||
{
|
||||
"filename": "GhostIcon@76x76.png",
|
||||
"idiom": "ipad",
|
||||
"scale": "1x",
|
||||
"size": "76x76"
|
||||
},
|
||||
{
|
||||
"filename": "GhostIcon@152x152.png",
|
||||
"idiom": "ipad",
|
||||
"scale": "2x",
|
||||
"size": "76x76"
|
||||
},
|
||||
{
|
||||
"filename": "GhostIcon@167x167.png",
|
||||
"idiom": "ipad",
|
||||
"scale": "2x",
|
||||
"size": "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"filename": "GhostIcon@1024x1024.png",
|
||||
"idiom": "ios-marketing",
|
||||
"scale": "1x",
|
||||
"size": "1024x1024"
|
||||
"filename" : "GhostIcon@1024x1024.png",
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
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 += '[[NSString alloc] initWithFormat:@"%@", arg{}]'.format(i)
|
||||
formatted_accessors += '''
|
||||
static _FormattedString * _Nonnull getFormatted{num_arguments}(_PresentationStrings * _Nonnull strings,
|
||||
static __attribute__((unused)) _FormattedString * _Nonnull getFormatted{num_arguments}(_PresentationStrings * _Nonnull strings,
|
||||
uint32_t keyId{arguments_string}) {{
|
||||
NSString *formatString = getSingle(strings, strings->_idToKey[@(keyId)], nil);
|
||||
NSArray<_FormattedStringRange *> *argumentRanges = extractArgumentRanges(formatString);
|
||||
|
||||
@@ -232,6 +232,9 @@ class BazelCommandLine:
|
||||
combined_arguments += self.common_debug_args
|
||||
combined_arguments += self.get_define_arguments()
|
||||
|
||||
if self.disable_provisioning_profiles:
|
||||
combined_arguments += ['--//Telegram:disableProvisioningProfiles']
|
||||
|
||||
if self.remote_cache is not None:
|
||||
combined_arguments += [
|
||||
'--remote_cache={}'.format(self.remote_cache),
|
||||
@@ -559,6 +562,8 @@ def generate_project(bazel, arguments):
|
||||
disable_extensions = arguments.disableExtensions
|
||||
if arguments.disableProvisioningProfiles is not None:
|
||||
disable_provisioning_profiles = arguments.disableProvisioningProfiles
|
||||
if disable_provisioning_profiles:
|
||||
bazel_command_line.set_disable_provisioning_profiles()
|
||||
if arguments.projectIncludeRelease is not None:
|
||||
project_include_release = arguments.projectIncludeRelease
|
||||
if arguments.xcodeManagedCodesigning is not None and arguments.xcodeManagedCodesigning == True:
|
||||
|
||||
@@ -26,6 +26,8 @@ def generate_xcodeproj(build_environment: BuildEnvironment, disable_extensions,
|
||||
if target_name == 'Telegram':
|
||||
if disable_extensions:
|
||||
bazel_generate_arguments += ['--//{}:disableExtensions'.format(app_target)]
|
||||
if disable_provisioning_profiles:
|
||||
bazel_generate_arguments += ['--//{}:disableProvisioningProfiles'.format(app_target)]
|
||||
bazel_generate_arguments += ['--//{}:disableStripping'.format(app_target)]
|
||||
|
||||
project_bazel_arguments = []
|
||||
@@ -35,6 +37,8 @@ def generate_xcodeproj(build_environment: BuildEnvironment, disable_extensions,
|
||||
if target_name == 'Telegram':
|
||||
if disable_extensions:
|
||||
project_bazel_arguments += ['--//{}:disableExtensions'.format(app_target)]
|
||||
if disable_provisioning_profiles:
|
||||
project_bazel_arguments += ['--//{}:disableProvisioningProfiles'.format(app_target)]
|
||||
project_bazel_arguments += ['--//{}:disableStripping'.format(app_target)]
|
||||
|
||||
project_bazel_arguments += ['--features=-swift.debug_prefix_map']
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"api_hash": "",
|
||||
"team_id": "",
|
||||
"app_center_id": "0",
|
||||
"is_internal_build": "false",
|
||||
"is_internal_build": "true",
|
||||
"is_appstore_build": "true",
|
||||
"appstore_id": "0",
|
||||
"app_specific_url_scheme": "ghostgram",
|
||||
|
||||
@@ -16,9 +16,11 @@ objc_library(
|
||||
], allow_empty=True) + private_headers,
|
||||
copts = [
|
||||
"-Werror",
|
||||
"-Wno-deprecated-declarations",
|
||||
],
|
||||
cxxopts = [
|
||||
"-Werror",
|
||||
"-Wno-deprecated-declarations",
|
||||
"-std=c++17",
|
||||
],
|
||||
hdrs = public_headers,
|
||||
|
||||
@@ -89,7 +89,7 @@ public class ChatListSearchItemNode: ListViewItemNode {
|
||||
required public init() {
|
||||
self.searchBarNode = SearchBarPlaceholderNode(fieldStyle: .modern)
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
super.init(layerBacked: false)
|
||||
|
||||
self.addSubnode(self.searchBarNode)
|
||||
}
|
||||
@@ -107,7 +107,6 @@ public class ChatListSearchItemNode: ListViewItemNode {
|
||||
}
|
||||
|
||||
public func asyncLayout() -> (_ item: ChatListSearchItem, _ params: ListViewItemLayoutParams, _ nextIsPinned: Bool, _ isEnabled: Bool) -> (ListViewItemNodeLayout, (Bool) -> Void) {
|
||||
let searchBarNodeLayout = self.searchBarNode.asyncLayout()
|
||||
let placeholder = self.placeholder
|
||||
|
||||
return { [weak self] item, params, nextIsPinned, isEnabled in
|
||||
@@ -115,9 +114,9 @@ public class ChatListSearchItemNode: ListViewItemNode {
|
||||
|
||||
let backgroundColor = nextIsPinned ? item.theme.chatList.pinnedItemBackgroundColor : item.theme.chatList.itemBackgroundColor
|
||||
let placeholderColor = item.theme.list.itemSecondaryTextColor
|
||||
let controlColor = item.theme.chat.inputPanel.panelControlColor
|
||||
|
||||
let placeholderString = NSAttributedString(string: placeholder ?? "", font: searchBarFont, textColor: placeholderColor)
|
||||
let (_, searchBarApply) = searchBarNodeLayout(placeholderString, placeholderString, CGSize(width: baseWidth - 20.0, height: 36.0), 1.0, placeholderColor, nextIsPinned ? item.theme.chatList.pinnedSearchBarColor : item.theme.chatList.regularSearchBarColor, backgroundColor, .immediate)
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 54.0), insets: UIEdgeInsets())
|
||||
|
||||
@@ -132,9 +131,7 @@ public class ChatListSearchItemNode: ListViewItemNode {
|
||||
|
||||
let searchBarFrame = CGRect(origin: CGPoint(x: params.leftInset + 10.0, y: 8.0), size: CGSize(width: baseWidth - 20.0, height: 36.0))
|
||||
strongSelf.searchBarNode.frame = searchBarFrame
|
||||
searchBarApply()
|
||||
|
||||
strongSelf.searchBarNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: baseWidth - 20.0, height: 36.0))
|
||||
_ = strongSelf.searchBarNode.updateLayout(placeholderString: placeholderString, compactPlaceholderString: placeholderString, constrainedSize: searchBarFrame.size, expansionProgress: 1.0, iconColor: placeholderColor, foregroundColor: nextIsPinned ? item.theme.chatList.pinnedSearchBarColor : item.theme.chatList.regularSearchBarColor, backgroundColor: backgroundColor, controlColor: controlColor, transition: transition)
|
||||
|
||||
if !item.isEnabled {
|
||||
if strongSelf.disabledOverlay == nil {
|
||||
|
||||
@@ -3,9 +3,14 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
swift_library(
|
||||
name = "ChatListUI",
|
||||
module_name = "ChatListUI",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
srcs = glob(
|
||||
[
|
||||
"Sources/**/*.swift",
|
||||
],
|
||||
exclude = [
|
||||
"Sources/ChatListFilterTabContainerNode.swift",
|
||||
],
|
||||
),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
|
||||
@@ -6595,8 +6595,17 @@ private final class ChatListLocationContext {
|
||||
var proxyButton: AnyComponentWithIdentity<NavigationButtonComponentEnvironment>?
|
||||
var storyButton: AnyComponentWithIdentity<NavigationButtonComponentEnvironment>?
|
||||
|
||||
// GHOSTGRAM: Account switcher — liquid glass avatar button for the next account
|
||||
var accountSwitcherButton: AnyComponentWithIdentity<NavigationButtonComponentEnvironment>?
|
||||
private var accountSwitcherDisposable: Disposable?
|
||||
private var accountSwitcherAvatarDisposable: Disposable?
|
||||
|
||||
var rightButtons: [AnyComponentWithIdentity<NavigationButtonComponentEnvironment>] {
|
||||
var result: [AnyComponentWithIdentity<NavigationButtonComponentEnvironment>] = []
|
||||
// Account switcher is first — leftmost of the right-side buttons
|
||||
if let accountSwitcherButton = self.accountSwitcherButton {
|
||||
result.append(accountSwitcherButton)
|
||||
}
|
||||
if let rightButton = self.rightButton {
|
||||
result.append(rightButton)
|
||||
}
|
||||
@@ -6631,6 +6640,90 @@ private final class ChatListLocationContext {
|
||||
self.location = location
|
||||
self.parentController = parentController
|
||||
|
||||
// GHOSTGRAM: Subscribe to account list and maintain the switcher button
|
||||
if case .chatList(.root) = location {
|
||||
self.accountSwitcherDisposable = (context.sharedContext.activeAccountsWithInfo
|
||||
|> deliverOnMainQueue)
|
||||
.start(next: { [weak self] (info: (primary: AccountRecordId?, accounts: [AccountWithInfo])) in
|
||||
guard let self else { return }
|
||||
|
||||
let primaryId = info.primary
|
||||
let accounts = info.accounts
|
||||
|
||||
// Only show when there is more than one account
|
||||
guard accounts.count > 1, let primaryId = primaryId else {
|
||||
if self.accountSwitcherButton != nil {
|
||||
self.accountSwitcherButton = nil
|
||||
let _ = self.parentController?.updateHeaderContent()
|
||||
self.parentController?.requestLayout(transition: .immediate)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Find next account cyclically
|
||||
let currentIndex = accounts.firstIndex(where: { $0.account.id == primaryId }) ?? 0
|
||||
let nextIndex = (currentIndex + 1) % accounts.count
|
||||
let nextAccount = accounts[nextIndex]
|
||||
let nextPeer = nextAccount.peer
|
||||
let nextPeerId = "\(nextAccount.account.id)"
|
||||
|
||||
// Build button placeholder immediately (image loads async)
|
||||
let buildButton: (UIImage?) -> Void = { [weak self] image in
|
||||
guard let self else { return }
|
||||
guard case .chatList(.root) = self.location else { return }
|
||||
|
||||
let sharedContext = self.context.sharedContext
|
||||
let nextAccountId = nextAccount.account.id
|
||||
|
||||
self.accountSwitcherButton = AnyComponentWithIdentity(
|
||||
id: "accountSwitcher",
|
||||
component: AnyComponent(NavigationButtonComponent(
|
||||
content: .avatar(peerId: nextPeerId, avatarImage: image),
|
||||
pressed: { [weak sharedContext] _ in
|
||||
sharedContext?.switchToAccount(id: nextAccountId, fromSettingsController: nil, withChatListController: nil)
|
||||
}
|
||||
))
|
||||
)
|
||||
// Trigger header rebuild
|
||||
let _ = self.parentController?.updateHeaderContent()
|
||||
self.parentController?.requestLayout(transition: .immediate)
|
||||
}
|
||||
|
||||
// Attempt to load the peer's avatar from mediaBox
|
||||
if let representation = nextPeer.smallProfileImage {
|
||||
self.accountSwitcherAvatarDisposable?.dispose()
|
||||
let resource = representation.resource
|
||||
let account = nextAccount.account
|
||||
|
||||
// Try to read cached data first; if not ready, trigger a fetch then watch for completion
|
||||
self.accountSwitcherAvatarDisposable = (account.postbox.mediaBox
|
||||
.resourceData(resource)
|
||||
|> deliverOnMainQueue)
|
||||
.start(next: { data in
|
||||
if data.complete, let uiImage = UIImage(contentsOfFile: data.path) {
|
||||
buildButton(uiImage)
|
||||
}
|
||||
}, completed: {
|
||||
// If resource was never complete after signal ended, show placeholder
|
||||
buildButton(nil)
|
||||
})
|
||||
|
||||
// Trigger the actual network fetch so mediaBox populates the resource
|
||||
if let peerReference = PeerReference(nextPeer) {
|
||||
let _ = fetchedMediaResource(
|
||||
mediaBox: account.postbox.mediaBox,
|
||||
userLocation: .peer(nextPeer.id),
|
||||
userContentType: .avatar,
|
||||
reference: .avatar(peer: peerReference, resource: resource)
|
||||
).start()
|
||||
}
|
||||
} else {
|
||||
// No photo — show placeholder
|
||||
buildButton(nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let hasProxy = context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.proxySettings])
|
||||
|> map { sharedData -> (Bool, Bool) in
|
||||
if let settings = sharedData.entries[SharedDataKeys.proxySettings]?.get(ProxySettings.self) {
|
||||
@@ -6949,6 +7042,8 @@ private final class ChatListLocationContext {
|
||||
deinit {
|
||||
self.titleDisposable?.dispose()
|
||||
self.stateDisposable?.dispose()
|
||||
self.accountSwitcherDisposable?.dispose()
|
||||
self.accountSwitcherAvatarDisposable?.dispose()
|
||||
}
|
||||
|
||||
private func updateChatList(
|
||||
@@ -6982,6 +7077,7 @@ private final class ChatListLocationContext {
|
||||
if case .chatList(.root) = self.location {
|
||||
self.rightButton = nil
|
||||
self.storyButton = nil
|
||||
self.accountSwitcherButton = nil
|
||||
}
|
||||
let title = !stateAndFilterId.state.selectedPeerIds.isEmpty ? presentationData.strings.ChatList_SelectedChats(Int32(stateAndFilterId.state.selectedPeerIds.count)) : defaultTitle
|
||||
|
||||
@@ -6997,6 +7093,7 @@ private final class ChatListLocationContext {
|
||||
if case .chatList(.root) = self.location {
|
||||
self.rightButton = nil
|
||||
self.storyButton = nil
|
||||
self.accountSwitcherButton = nil
|
||||
}
|
||||
self.leftButton = AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent(
|
||||
content: .text(title: presentationData.strings.Common_Done, isBold: true),
|
||||
|
||||
@@ -14,6 +14,7 @@ import MergedAvatarsNode
|
||||
import TextNodeWithEntities
|
||||
import TextFormat
|
||||
import AvatarNode
|
||||
import GlobalControlPanelsContext
|
||||
|
||||
class ChatListNoticeItem: ListViewItem {
|
||||
enum Action {
|
||||
@@ -25,12 +26,12 @@ class ChatListNoticeItem: ListViewItem {
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let notice: ChatListNotice
|
||||
let notice: GlobalControlPanelsContext.ChatListNotice
|
||||
let action: (Action) -> Void
|
||||
|
||||
let selectable: Bool = true
|
||||
|
||||
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, notice: ChatListNotice, action: @escaping (Action) -> Void) {
|
||||
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, notice: GlobalControlPanelsContext.ChatListNotice, action: @escaping (Action) -> Void) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
@@ -130,7 +131,7 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode {
|
||||
self.arrowNode = ASImageNode()
|
||||
self.separatorNode = ASDisplayNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
|
||||
super.init(layerBacked: false, rotated: false, seeThrough: false)
|
||||
|
||||
self.contentContainer.clipsToBounds = true
|
||||
self.clipsToBounds = true
|
||||
|
||||
@@ -122,10 +122,6 @@ public func navigationBarBackArrowImage(color: UIColor) -> UIImage? {
|
||||
}
|
||||
}
|
||||
|
||||
public protocol NavigationButtonCustomDisplayNode {
|
||||
var isHighlightable: Bool { get }
|
||||
}
|
||||
|
||||
public protocol NavigationButtonNode: ASDisplayNode {
|
||||
func updateManualAlpha(alpha: CGFloat, transition: ContainedViewLayoutTransition)
|
||||
var mainContentNode: ASDisplayNode? { get }
|
||||
|
||||
@@ -328,7 +328,7 @@ private final class NavigationButtonItemNode: ImmediateTextNode {
|
||||
}
|
||||
|
||||
|
||||
public final class NavigationButtonNode: ContextControllerSourceNode {
|
||||
public final class NavigationButtonNodeImpl: ContextControllerSourceNode, NavigationButtonNode {
|
||||
private var nodes: [NavigationButtonItemNode] = []
|
||||
|
||||
private var disappearingNodes: [(frame: CGRect, size: CGSize, node: NavigationButtonItemNode)] = []
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import Postbox
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
import AccountContext
|
||||
import ComponentFlow
|
||||
import SliderComponent
|
||||
|
||||
private let minDeletedMessageTransparencyPercent: Int32 = Int32(AntiDeleteManager.minDeletedMessageTransparency * 100.0)
|
||||
private let maxDeletedMessageTransparencyPercent: Int32 = Int32(AntiDeleteManager.maxDeletedMessageTransparency * 100.0)
|
||||
|
||||
private func clampDeletedMessageTransparencyPercent(_ value: Int32) -> Int32 {
|
||||
return max(minDeletedMessageTransparencyPercent, min(maxDeletedMessageTransparencyPercent, value))
|
||||
}
|
||||
|
||||
// MARK: - Entry Definition
|
||||
|
||||
@@ -17,6 +27,7 @@ private enum DeletedMessagesSection: Int32 {
|
||||
private enum DeletedMessagesEntry: ItemListNodeEntry {
|
||||
case enableToggle(PresentationTheme, String, Bool)
|
||||
case archiveMediaToggle(PresentationTheme, String, Bool)
|
||||
case transparencySlider(PresentationTheme, Int32, Bool)
|
||||
case settingsInfo(PresentationTheme, String)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
@@ -29,8 +40,10 @@ private enum DeletedMessagesEntry: ItemListNodeEntry {
|
||||
return 0
|
||||
case .archiveMediaToggle:
|
||||
return 1
|
||||
case .settingsInfo:
|
||||
case .transparencySlider:
|
||||
return 2
|
||||
case .settingsInfo:
|
||||
return 3
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +61,12 @@ private enum DeletedMessagesEntry: ItemListNodeEntry {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .transparencySlider(lhsTheme, lhsValue, lhsIsEnabled):
|
||||
if case let .transparencySlider(rhsTheme, rhsValue, rhsIsEnabled) = rhs,
|
||||
lhsTheme === rhsTheme, lhsValue == rhsValue, lhsIsEnabled == rhsIsEnabled {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .settingsInfo(lhsTheme, lhsText):
|
||||
if case let .settingsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
@@ -85,6 +104,16 @@ private enum DeletedMessagesEntry: ItemListNodeEntry {
|
||||
arguments.toggleArchiveMedia(value)
|
||||
}
|
||||
)
|
||||
case let .transparencySlider(theme, value, isEnabled):
|
||||
return DeletedMessagesTransparencySliderItem(
|
||||
theme: theme,
|
||||
value: value,
|
||||
isEnabled: isEnabled,
|
||||
sectionId: self.section,
|
||||
updated: { value in
|
||||
arguments.updateTransparency(value)
|
||||
}
|
||||
)
|
||||
case let .settingsInfo(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
}
|
||||
@@ -96,13 +125,16 @@ private enum DeletedMessagesEntry: ItemListNodeEntry {
|
||||
private final class DeletedMessagesControllerArguments {
|
||||
let toggleEnabled: (Bool) -> Void
|
||||
let toggleArchiveMedia: (Bool) -> Void
|
||||
let updateTransparency: (Int32) -> Void
|
||||
|
||||
init(
|
||||
toggleEnabled: @escaping (Bool) -> Void,
|
||||
toggleArchiveMedia: @escaping (Bool) -> Void
|
||||
toggleArchiveMedia: @escaping (Bool) -> Void,
|
||||
updateTransparency: @escaping (Int32) -> Void
|
||||
) {
|
||||
self.toggleEnabled = toggleEnabled
|
||||
self.toggleArchiveMedia = toggleArchiveMedia
|
||||
self.updateTransparency = updateTransparency
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,10 +143,12 @@ private final class DeletedMessagesControllerArguments {
|
||||
private struct DeletedMessagesControllerState: Equatable {
|
||||
var isEnabled: Bool
|
||||
var archiveMedia: Bool
|
||||
var transparencyPercent: Int32
|
||||
|
||||
static func ==(lhs: DeletedMessagesControllerState, rhs: DeletedMessagesControllerState) -> Bool {
|
||||
return lhs.isEnabled == rhs.isEnabled &&
|
||||
lhs.archiveMedia == rhs.archiveMedia
|
||||
lhs.archiveMedia == rhs.archiveMedia &&
|
||||
lhs.transparencyPercent == rhs.transparencyPercent
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +162,8 @@ private func deletedMessagesControllerEntries(
|
||||
|
||||
entries.append(.enableToggle(presentationData.theme, "Сохранять удалённые сообщения", state.isEnabled))
|
||||
entries.append(.archiveMediaToggle(presentationData.theme, "Архивировать медиа", state.archiveMedia))
|
||||
entries.append(.settingsInfo(presentationData.theme, "Когда включено, сообщения, удалённые другими пользователями, будут сохраняться локально. Рядом со временем сообщения появится иконка корзины."))
|
||||
entries.append(.transparencySlider(presentationData.theme, state.transparencyPercent, state.isEnabled))
|
||||
entries.append(.settingsInfo(presentationData.theme, "Когда включено, сообщения, удалённые другими пользователями, будут сохраняться локально. Прозрачность влияет только на сообщения, которые уже помечены как удалённые."))
|
||||
|
||||
return entries
|
||||
}
|
||||
@@ -138,7 +173,8 @@ private func deletedMessagesControllerEntries(
|
||||
public func deletedMessagesController(context: AccountContext) -> ViewController {
|
||||
let initialState = DeletedMessagesControllerState(
|
||||
isEnabled: AntiDeleteManager.shared.isEnabled,
|
||||
archiveMedia: AntiDeleteManager.shared.archiveMedia
|
||||
archiveMedia: AntiDeleteManager.shared.archiveMedia,
|
||||
transparencyPercent: clampDeletedMessageTransparencyPercent(Int32(round(AntiDeleteManager.shared.deletedMessageTransparency * 100.0)))
|
||||
)
|
||||
|
||||
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
|
||||
@@ -163,6 +199,15 @@ public func deletedMessagesController(context: AccountContext) -> ViewController
|
||||
state.archiveMedia = value
|
||||
return state
|
||||
}
|
||||
},
|
||||
updateTransparency: { value in
|
||||
let clampedValue = clampDeletedMessageTransparencyPercent(value)
|
||||
AntiDeleteManager.shared.deletedMessageTransparency = Double(clampedValue) / 100.0
|
||||
updateState { state in
|
||||
var state = state
|
||||
state.transparencyPercent = clampedValue
|
||||
return state
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -195,3 +240,225 @@ public func deletedMessagesController(context: AccountContext) -> ViewController
|
||||
let controller = ItemListController(context: context, state: signal)
|
||||
return controller
|
||||
}
|
||||
|
||||
private final class DeletedMessagesTransparencySliderItem: ListViewItem, ItemListItem {
|
||||
let theme: PresentationTheme
|
||||
let value: Int32
|
||||
let isEnabled: Bool
|
||||
let sectionId: ItemListSectionId
|
||||
let updated: (Int32) -> Void
|
||||
|
||||
init(theme: PresentationTheme, value: Int32, isEnabled: Bool, sectionId: ItemListSectionId, updated: @escaping (Int32) -> Void) {
|
||||
self.theme = theme
|
||||
self.value = clampDeletedMessageTransparencyPercent(value)
|
||||
self.isEnabled = isEnabled
|
||||
self.sectionId = sectionId
|
||||
self.updated = updated
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = DeletedMessagesTransparencySliderItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in apply() })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
if let nodeValue = node() as? DeletedMessagesTransparencySliderItemNode {
|
||||
let makeLayout = nodeValue.asyncLayout()
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class DeletedMessagesTransparencySliderItemNode: ListViewItemNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
private let bottomStripeNode: ASDisplayNode
|
||||
private let maskNode: ASImageNode
|
||||
|
||||
private let leftTextNode: ImmediateTextNode
|
||||
private let rightTextNode: ImmediateTextNode
|
||||
private let centerTextNode: ImmediateTextNode
|
||||
private let slider = ComponentView<Empty>()
|
||||
|
||||
private var item: DeletedMessagesTransparencySliderItem?
|
||||
|
||||
init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
|
||||
self.topStripeNode = ASDisplayNode()
|
||||
self.topStripeNode.isLayerBacked = true
|
||||
|
||||
self.bottomStripeNode = ASDisplayNode()
|
||||
self.bottomStripeNode.isLayerBacked = true
|
||||
|
||||
self.maskNode = ASImageNode()
|
||||
|
||||
self.leftTextNode = ImmediateTextNode()
|
||||
self.rightTextNode = ImmediateTextNode()
|
||||
self.centerTextNode = ImmediateTextNode()
|
||||
|
||||
super.init(layerBacked: false)
|
||||
|
||||
self.addSubnode(self.leftTextNode)
|
||||
self.addSubnode(self.rightTextNode)
|
||||
self.addSubnode(self.centerTextNode)
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: DeletedMessagesTransparencySliderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
return { item, params, neighbors in
|
||||
let separatorHeight = UIScreenPixel
|
||||
let contentSize = CGSize(width: params.width, height: 88.0)
|
||||
let insets = itemListNeighborsGroupedInsets(neighbors, params)
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
let layoutSize = layout.size
|
||||
|
||||
return (layout, { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.item = item
|
||||
|
||||
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
|
||||
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
|
||||
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
|
||||
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
}
|
||||
if strongSelf.topStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
||||
}
|
||||
if strongSelf.bottomStripeNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
||||
}
|
||||
if strongSelf.maskNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
|
||||
}
|
||||
|
||||
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||
var hasTopCorners = false
|
||||
var hasBottomCorners = false
|
||||
|
||||
switch neighbors.top {
|
||||
case .sameSection(false):
|
||||
strongSelf.topStripeNode.isHidden = true
|
||||
default:
|
||||
hasTopCorners = true
|
||||
strongSelf.topStripeNode.isHidden = hasCorners
|
||||
}
|
||||
|
||||
let bottomStripeInset: CGFloat
|
||||
let bottomStripeOffset: CGFloat
|
||||
switch neighbors.bottom {
|
||||
case .sameSection(false):
|
||||
bottomStripeInset = 0.0
|
||||
bottomStripeOffset = -separatorHeight
|
||||
strongSelf.bottomStripeNode.isHidden = false
|
||||
default:
|
||||
bottomStripeInset = 0.0
|
||||
bottomStripeOffset = 0.0
|
||||
hasBottomCorners = true
|
||||
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||||
}
|
||||
|
||||
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
|
||||
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
|
||||
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
|
||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
|
||||
|
||||
let sideTextColor = item.theme.list.itemSecondaryTextColor.withAlphaComponent(item.isEnabled ? 1.0 : 0.6)
|
||||
let centerTextColor = item.isEnabled ? item.theme.list.itemPrimaryTextColor : item.theme.list.itemDisabledTextColor
|
||||
|
||||
strongSelf.leftTextNode.attributedText = NSAttributedString(string: "Меньше", font: Font.regular(13.0), textColor: sideTextColor)
|
||||
strongSelf.rightTextNode.attributedText = NSAttributedString(string: "Больше", font: Font.regular(13.0), textColor: sideTextColor)
|
||||
strongSelf.centerTextNode.attributedText = NSAttributedString(string: "Прозрачность \(item.value)%", font: Font.regular(16.0), textColor: centerTextColor)
|
||||
|
||||
let leftTextSize = strongSelf.leftTextNode.updateLayout(CGSize(width: 120.0, height: 100.0))
|
||||
let rightTextSize = strongSelf.rightTextNode.updateLayout(CGSize(width: 120.0, height: 100.0))
|
||||
let centerTextSize = strongSelf.centerTextNode.updateLayout(CGSize(width: params.width - params.leftInset - params.rightInset - 60.0, height: 100.0))
|
||||
|
||||
let sideInset: CGFloat = 18.0
|
||||
strongSelf.leftTextNode.frame = CGRect(origin: CGPoint(x: params.leftInset + sideInset, y: 15.0), size: leftTextSize)
|
||||
strongSelf.rightTextNode.frame = CGRect(origin: CGPoint(x: params.width - params.leftInset - sideInset - rightTextSize.width, y: 15.0), size: rightTextSize)
|
||||
strongSelf.centerTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - centerTextSize.width) / 2.0), y: 11.0), size: centerTextSize)
|
||||
|
||||
let maxRange = CGFloat(maxDeletedMessageTransparencyPercent - minDeletedMessageTransparencyPercent)
|
||||
let normalizedValue: CGFloat
|
||||
if maxRange.isZero {
|
||||
normalizedValue = 0.0
|
||||
} else {
|
||||
normalizedValue = CGFloat(item.value - minDeletedMessageTransparencyPercent) / maxRange
|
||||
}
|
||||
|
||||
let sliderSize = strongSelf.slider.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
SliderComponent(
|
||||
content: .continuous(.init(
|
||||
value: normalizedValue,
|
||||
minValue: nil,
|
||||
valueUpdated: { [weak self] value in
|
||||
guard let self, let item = self.item, item.isEnabled else {
|
||||
return
|
||||
}
|
||||
|
||||
let transparencyValue = Int32((CGFloat(minDeletedMessageTransparencyPercent) + maxRange * value).rounded())
|
||||
item.updated(clampDeletedMessageTransparencyPercent(transparencyValue))
|
||||
}
|
||||
)),
|
||||
useNative: true,
|
||||
trackBackgroundColor: item.theme.list.itemSwitchColors.frameColor,
|
||||
trackForegroundColor: item.isEnabled ? item.theme.list.itemAccentColor : item.theme.list.itemDisabledTextColor
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0)
|
||||
)
|
||||
|
||||
if let sliderView = strongSelf.slider.view {
|
||||
if sliderView.superview == nil {
|
||||
strongSelf.view.addSubview(sliderView)
|
||||
}
|
||||
|
||||
sliderView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - sliderSize.width) / 2.0), y: 36.0), size: sliderSize)
|
||||
sliderView.isUserInteractionEnabled = item.isEnabled
|
||||
sliderView.alpha = item.isEnabled ? 1.0 : 0.55
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ private enum GhostgramSettingsEntry: ItemListNodeEntry {
|
||||
case misc(PresentationTheme, String, String)
|
||||
case deviceSpoof(PresentationTheme, String, String)
|
||||
case voiceMorpher(PresentationTheme, String, String)
|
||||
case sendDelay(PresentationTheme, String, String)
|
||||
case info(PresentationTheme, String)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
@@ -37,8 +38,10 @@ private enum GhostgramSettingsEntry: ItemListNodeEntry {
|
||||
return 3
|
||||
case .voiceMorpher:
|
||||
return 4
|
||||
case .info:
|
||||
case .sendDelay:
|
||||
return 5
|
||||
case .info:
|
||||
return 6
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +77,12 @@ private enum GhostgramSettingsEntry: ItemListNodeEntry {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .sendDelay(lhsTheme, lhsText, lhsValue):
|
||||
if case let .sendDelay(rhsTheme, rhsText, rhsValue) = rhs,
|
||||
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .info(lhsTheme, lhsText):
|
||||
if case let .info(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
@@ -144,6 +153,17 @@ private enum GhostgramSettingsEntry: ItemListNodeEntry {
|
||||
arguments.openVoiceMorpher()
|
||||
}
|
||||
)
|
||||
case let .sendDelay(_, text, value):
|
||||
return ItemListDisclosureItem(
|
||||
presentationData: presentationData,
|
||||
title: text,
|
||||
label: value,
|
||||
sectionId: self.section,
|
||||
style: .blocks,
|
||||
action: {
|
||||
arguments.openSendDelay()
|
||||
}
|
||||
)
|
||||
case let .info(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
}
|
||||
@@ -158,19 +178,22 @@ private final class GhostgramSettingsControllerArguments {
|
||||
let openMisc: () -> Void
|
||||
let openDeviceSpoof: () -> Void
|
||||
let openVoiceMorpher: () -> Void
|
||||
let openSendDelay: () -> Void
|
||||
|
||||
init(
|
||||
openDeletedMessages: @escaping () -> Void,
|
||||
openGhostMode: @escaping () -> Void,
|
||||
openMisc: @escaping () -> Void,
|
||||
openDeviceSpoof: @escaping () -> Void,
|
||||
openVoiceMorpher: @escaping () -> Void
|
||||
openVoiceMorpher: @escaping () -> Void,
|
||||
openSendDelay: @escaping () -> Void
|
||||
) {
|
||||
self.openDeletedMessages = openDeletedMessages
|
||||
self.openGhostMode = openGhostMode
|
||||
self.openMisc = openMisc
|
||||
self.openDeviceSpoof = openDeviceSpoof
|
||||
self.openVoiceMorpher = openVoiceMorpher
|
||||
self.openSendDelay = openSendDelay
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +207,8 @@ private struct GhostgramSettingsState: Equatable {
|
||||
var miscActiveCount: Int
|
||||
var deviceSpoofEnabled: Bool
|
||||
var voiceMorpherEnabled: Bool
|
||||
var voiceMorpherPresetName: String
|
||||
var sendDelayEnabled: Bool
|
||||
|
||||
static func current() -> GhostgramSettingsState {
|
||||
return GhostgramSettingsState(
|
||||
@@ -193,7 +218,9 @@ private struct GhostgramSettingsState: Equatable {
|
||||
miscEnabled: MiscSettingsManager.shared.isEnabled,
|
||||
miscActiveCount: MiscSettingsManager.shared.activeFeatureCount,
|
||||
deviceSpoofEnabled: DeviceSpoofManager.shared.isEnabled,
|
||||
voiceMorpherEnabled: VoiceMorpherManager.shared.isEnabled
|
||||
voiceMorpherEnabled: VoiceMorpherManager.shared.isEnabled,
|
||||
voiceMorpherPresetName: VoiceMorpherManager.shared.selectedPreset.name,
|
||||
sendDelayEnabled: SendDelayManager.shared.isEnabled
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -223,9 +250,13 @@ private func ghostgramSettingsControllerEntries(
|
||||
entries.append(.deviceSpoof(presentationData.theme, "Подмена устройства", deviceSpoofStatus))
|
||||
|
||||
// Voice Morpher
|
||||
let voiceMorpherStatus = state.voiceMorpherEnabled ? VoiceMorpherManager.shared.selectedPreset.name : "Выкл"
|
||||
let voiceMorpherStatus = state.voiceMorpherEnabled ? state.voiceMorpherPresetName : "Выкл"
|
||||
entries.append(.voiceMorpher(presentationData.theme, "Голосовой двойник", voiceMorpherStatus))
|
||||
|
||||
// Send Delay
|
||||
let sendDelayStatus = state.sendDelayEnabled ? "Вкл" : "Выкл"
|
||||
entries.append(.sendDelay(presentationData.theme, "Отложка сообщений", sendDelayStatus))
|
||||
|
||||
// Info
|
||||
entries.append(.info(presentationData.theme, "Функции конфиденциальности Ghostgram. Скрытые отметки о прочтении, обход исчезающих сообщений, обход защиты от пересылки и другое."))
|
||||
|
||||
@@ -255,6 +286,9 @@ public func ghostgramSettingsController(context: AccountContext) -> ViewControll
|
||||
},
|
||||
openVoiceMorpher: {
|
||||
pushControllerImpl?(voiceMorpherController(context: context), true)
|
||||
},
|
||||
openSendDelay: {
|
||||
pushControllerImpl?(sendDelayController(context: context), true)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -328,7 +328,7 @@ public func miscController(context: AccountContext) -> ViewController {
|
||||
|
||||
let controllerState = ItemListControllerState(
|
||||
presentationData: ItemListPresentationData(presentationData),
|
||||
title: .text("Misc"),
|
||||
title: .text("Прочее"),
|
||||
leftNavigationButton: nil,
|
||||
rightNavigationButton: nil,
|
||||
backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back),
|
||||
|
||||
@@ -90,7 +90,7 @@ class NotificationSearchItemNode: ListViewItemNode {
|
||||
required init() {
|
||||
self.searchBarNode = SearchBarPlaceholderNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
super.init(layerBacked: false)
|
||||
|
||||
self.addSubnode(self.searchBarNode)
|
||||
}
|
||||
@@ -104,16 +104,16 @@ class NotificationSearchItemNode: ListViewItemNode {
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: NotificationSearchItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) {
|
||||
let searchBarNodeLayout = self.searchBarNode.asyncLayout()
|
||||
let placeholder = self.placeholder
|
||||
|
||||
return { item, params in
|
||||
let baseWidth = params.width - params.leftInset - params.rightInset
|
||||
|
||||
let backgroundColor = item.theme.chatList.itemBackgroundColor
|
||||
let iconColor = UIColor(rgb: 0x8e8e93)
|
||||
let controlColor = item.theme.chat.inputPanel.panelControlColor
|
||||
|
||||
let placeholderString = NSAttributedString(string: placeholder ?? "", font: searchBarFont, textColor: UIColor(rgb: 0x8e8e93))
|
||||
let (_, searchBarApply) = searchBarNodeLayout(placeholderString, placeholderString, CGSize(width: baseWidth - 16.0, height: 28.0), 1.0, UIColor(rgb: 0x8e8e93), item.theme.chatList.regularSearchBarColor, backgroundColor, .immediate)
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 44.0), insets: UIEdgeInsets())
|
||||
|
||||
@@ -126,10 +126,9 @@ class NotificationSearchItemNode: ListViewItemNode {
|
||||
transition = .immediate
|
||||
}
|
||||
|
||||
strongSelf.searchBarNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 8.0, y: 8.0), size: CGSize(width: baseWidth - 16.0, height: 28.0))
|
||||
searchBarApply()
|
||||
|
||||
strongSelf.searchBarNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: baseWidth - 16.0, height: 28.0))
|
||||
let searchBarSize = CGSize(width: baseWidth - 16.0, height: 28.0)
|
||||
strongSelf.searchBarNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 8.0, y: 8.0), size: searchBarSize)
|
||||
_ = strongSelf.searchBarNode.updateLayout(placeholderString: placeholderString, compactPlaceholderString: placeholderString, constrainedSize: searchBarSize, expansionProgress: 1.0, iconColor: iconColor, foregroundColor: item.theme.chatList.regularSearchBarColor, backgroundColor: backgroundColor, controlColor: controlColor, transition: transition)
|
||||
|
||||
transition.updateBackgroundColor(node: strongSelf, color: backgroundColor)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
import AccountContext
|
||||
|
||||
// MARK: - Section / Entry definitions
|
||||
|
||||
private enum SendDelaySection: Int32 {
|
||||
case main
|
||||
}
|
||||
|
||||
private enum SendDelayEntry: ItemListNodeEntry {
|
||||
case toggle(PresentationTheme, String, Bool)
|
||||
case info(PresentationTheme, String)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
return SendDelaySection.main.rawValue
|
||||
}
|
||||
|
||||
var stableId: Int32 {
|
||||
switch self {
|
||||
case .toggle: return 0
|
||||
case .info: return 1
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: SendDelayEntry, rhs: SendDelayEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .toggle(lhsTheme, lhsText, lhsValue):
|
||||
if case let .toggle(rhsTheme, rhsText, rhsValue) = rhs,
|
||||
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case let .info(lhsTheme, lhsText):
|
||||
if case let .info(rhsTheme, rhsText) = rhs,
|
||||
lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: SendDelayEntry, rhs: SendDelayEntry) -> Bool {
|
||||
return lhs.stableId < rhs.stableId
|
||||
}
|
||||
|
||||
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||
let arguments = arguments as! SendDelayControllerArguments
|
||||
switch self {
|
||||
case let .toggle(_, text, value):
|
||||
return ItemListSwitchItem(
|
||||
presentationData: presentationData,
|
||||
title: text,
|
||||
value: value,
|
||||
sectionId: self.section,
|
||||
style: .blocks,
|
||||
updated: { newValue in
|
||||
arguments.toggleEnabled(newValue)
|
||||
}
|
||||
)
|
||||
case let .info(_, text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Arguments
|
||||
|
||||
private final class SendDelayControllerArguments {
|
||||
let toggleEnabled: (Bool) -> Void
|
||||
init(toggleEnabled: @escaping (Bool) -> Void) {
|
||||
self.toggleEnabled = toggleEnabled
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - State
|
||||
|
||||
private struct SendDelayControllerState: Equatable {
|
||||
var isEnabled: Bool
|
||||
}
|
||||
|
||||
// MARK: - Entries builder
|
||||
|
||||
private func sendDelayControllerEntries(
|
||||
presentationData: PresentationData,
|
||||
state: SendDelayControllerState
|
||||
) -> [SendDelayEntry] {
|
||||
let theme = presentationData.theme
|
||||
return [
|
||||
.toggle(theme, "Использовать отложку", state.isEnabled),
|
||||
.info(theme, "Автоматически ставит задержку в ~12 секунд (дольше для сообщений с вложениями) при отправке сообщений. При использовании этой функции вы не будете появляться в сети.")
|
||||
]
|
||||
}
|
||||
|
||||
// MARK: - Controller
|
||||
|
||||
public func sendDelayController(context: AccountContext) -> ViewController {
|
||||
let statePromise = ValuePromise(
|
||||
SendDelayControllerState(isEnabled: SendDelayManager.shared.isEnabled),
|
||||
ignoreRepeated: true
|
||||
)
|
||||
let stateValue = Atomic(value: SendDelayControllerState(isEnabled: SendDelayManager.shared.isEnabled))
|
||||
|
||||
let updateState: ((inout SendDelayControllerState) -> Void) -> Void = { f in
|
||||
let result = stateValue.modify { state in
|
||||
var s = state; f(&s); return s
|
||||
}
|
||||
statePromise.set(result)
|
||||
}
|
||||
|
||||
let arguments = SendDelayControllerArguments(
|
||||
toggleEnabled: { value in
|
||||
SendDelayManager.shared.isEnabled = value
|
||||
updateState { $0.isEnabled = value }
|
||||
}
|
||||
)
|
||||
|
||||
let signal = combineLatest(
|
||||
context.sharedContext.presentationData,
|
||||
statePromise.get()
|
||||
)
|
||||
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
let entries = sendDelayControllerEntries(presentationData: presentationData, state: state)
|
||||
|
||||
let controllerState = ItemListControllerState(
|
||||
presentationData: ItemListPresentationData(presentationData),
|
||||
title: .text("Отложка сообщений"),
|
||||
leftNavigationButton: nil,
|
||||
rightNavigationButton: nil,
|
||||
backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back),
|
||||
animateChanges: false
|
||||
)
|
||||
let listState = ItemListNodeState(
|
||||
presentationData: ItemListPresentationData(presentationData),
|
||||
entries: entries,
|
||||
style: .blocks,
|
||||
animateChanges: true
|
||||
)
|
||||
return (controllerState, (listState, arguments))
|
||||
}
|
||||
|
||||
return ItemListController(context: context, state: signal)
|
||||
}
|
||||
@@ -22,7 +22,7 @@ private let pink = UIColor(rgb: 0xef436c)
|
||||
private let latePurple = UIColor(rgb: 0xaa56a6)
|
||||
private let latePink = UIColor(rgb: 0xef476f)
|
||||
|
||||
private func textForTimeout(value: Int32) -> String {
|
||||
private func callStatusBarTextForTimeout(value: Int32) -> String {
|
||||
if value < 3600 {
|
||||
let minutes = value / 60
|
||||
let seconds = value % 60
|
||||
@@ -498,9 +498,9 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
|
||||
timerText = presentationData.strings.VoiceChat_StatusStartsIn(scheduledTimeIntervalString(strings: presentationData.strings, value: elapsedTime)).string
|
||||
} else if elapsedTime < 0 {
|
||||
isLate = true
|
||||
timerText = presentationData.strings.VoiceChat_StatusLateBy(textForTimeout(value: abs(elapsedTime))).string
|
||||
timerText = presentationData.strings.VoiceChat_StatusLateBy(callStatusBarTextForTimeout(value: abs(elapsedTime))).string
|
||||
} else {
|
||||
timerText = presentationData.strings.VoiceChat_StatusStartsIn(textForTimeout(value: elapsedTime)).string
|
||||
timerText = presentationData.strings.VoiceChat_StatusStartsIn(callStatusBarTextForTimeout(value: elapsedTime)).string
|
||||
}
|
||||
segments.append(.text(0, NSAttributedString(string: timerText, font: textFont, textColor: textColor)))
|
||||
} else if let membersCount = membersCount {
|
||||
|
||||
@@ -13,7 +13,7 @@ private let pink = UIColor(rgb: 0xef436c)
|
||||
private let latePurple = UIColor(rgb: 0x974aa9)
|
||||
private let latePink = UIColor(rgb: 0xf0436c)
|
||||
|
||||
private func textForTimeout(value: Int32) -> String {
|
||||
private func scheduledInfoTextForTimeout(value: Int32) -> String {
|
||||
if value < 3600 {
|
||||
let minutes = value / 60
|
||||
let seconds = value % 60
|
||||
@@ -150,7 +150,7 @@ final class VideoChatScheduledInfoComponent: Component {
|
||||
if remainingSeconds >= 86400 {
|
||||
countdownText = scheduledTimeIntervalString(strings: component.strings, value: remainingSeconds)
|
||||
} else {
|
||||
countdownText = textForTimeout(value: abs(remainingSeconds))
|
||||
countdownText = scheduledInfoTextForTimeout(value: abs(remainingSeconds))
|
||||
/*if remainingSeconds < 0 && !self.isLate {
|
||||
self.isLate = true
|
||||
self.foregroundGradientLayer.colors = [latePink.cgColor, latePurple.cgColor, latePurple.cgColor]
|
||||
|
||||
@@ -12,7 +12,7 @@ private let pink = UIColor(rgb: 0xef436c)
|
||||
private let latePurple = UIColor(rgb: 0x974aa9)
|
||||
private let latePink = UIColor(rgb: 0xf0436c)
|
||||
|
||||
private func textForTimeout(value: Int32) -> String {
|
||||
private func voiceChatTimerTextForTimeout(value: Int32) -> String {
|
||||
if value < 3600 {
|
||||
let minutes = value / 60
|
||||
let seconds = value % 60
|
||||
@@ -189,7 +189,7 @@ final class VoiceChatTimerNode: ASDisplayNode {
|
||||
if elapsedTime >= 86400 {
|
||||
timerText = scheduledTimeIntervalString(strings: self.strings, value: elapsedTime)
|
||||
} else {
|
||||
timerText = textForTimeout(value: abs(elapsedTime))
|
||||
timerText = voiceChatTimerTextForTimeout(value: abs(elapsedTime))
|
||||
if elapsedTime < 0 && !self.isLate {
|
||||
self.isLate = true
|
||||
self.foregroundGradientLayer.colors = [latePink.cgColor, latePurple.cgColor, latePurple.cgColor]
|
||||
|
||||
@@ -11,6 +11,7 @@ public final class AntiDeleteManager {
|
||||
private let defaults = UserDefaults.standard
|
||||
private let enabledKey = "antiDelete.enabled"
|
||||
private let archiveMediaKey = "antiDelete.archiveMedia"
|
||||
private let deletedMessageTransparencyKey = "antiDelete.deletedMessageTransparency"
|
||||
private let archiveKey = "antiDelete.archive"
|
||||
private let deletedIdsKey = "antiDelete.deletedIds"
|
||||
|
||||
@@ -26,6 +27,33 @@ public final class AntiDeleteManager {
|
||||
set { defaults.set(newValue, forKey: archiveMediaKey) }
|
||||
}
|
||||
|
||||
/// Минимальное значение прозрачности удалённого сообщения
|
||||
public static let minDeletedMessageTransparency: Double = 0.0
|
||||
|
||||
/// Максимальное значение прозрачности удалённого сообщения
|
||||
public static let maxDeletedMessageTransparency: Double = 0.8
|
||||
|
||||
/// Значение прозрачности удалённого сообщения по умолчанию
|
||||
public static let defaultDeletedMessageTransparency: Double = 0.45
|
||||
|
||||
/// Прозрачность удалённых сообщений (0.0 = непрозрачно, 0.8 = максимально прозрачно)
|
||||
public var deletedMessageTransparency: Double {
|
||||
get {
|
||||
let value = defaults.object(forKey: deletedMessageTransparencyKey) as? NSNumber
|
||||
let resolvedValue = value?.doubleValue ?? Self.defaultDeletedMessageTransparency
|
||||
return max(Self.minDeletedMessageTransparency, min(Self.maxDeletedMessageTransparency, resolvedValue))
|
||||
}
|
||||
set {
|
||||
let clampedValue = max(Self.minDeletedMessageTransparency, min(Self.maxDeletedMessageTransparency, newValue))
|
||||
defaults.set(clampedValue, forKey: deletedMessageTransparencyKey)
|
||||
}
|
||||
}
|
||||
|
||||
/// Альфа для отображения удалённых сообщений
|
||||
public var deletedMessageDisplayAlpha: Double {
|
||||
return 1.0 - self.deletedMessageTransparency
|
||||
}
|
||||
|
||||
// MARK: - Deleted Message IDs Storage
|
||||
|
||||
private var deletedMessageIds: Set<String> = []
|
||||
@@ -120,6 +148,9 @@ public final class AntiDeleteManager {
|
||||
if defaults.object(forKey: archiveMediaKey) == nil {
|
||||
defaults.set(true, forKey: archiveMediaKey)
|
||||
}
|
||||
if defaults.object(forKey: deletedMessageTransparencyKey) == nil {
|
||||
defaults.set(Self.defaultDeletedMessageTransparency, forKey: deletedMessageTransparencyKey)
|
||||
}
|
||||
loadArchive()
|
||||
loadDeletedIds()
|
||||
}
|
||||
|
||||
@@ -23,9 +23,6 @@ public final class GhostModeManager {
|
||||
|
||||
private let defaults = UserDefaults.standard
|
||||
|
||||
// Prevents recursive mutual-exclusion calls
|
||||
private var isApplyingMutualExclusion = false
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
/// Master toggle for Ghost Mode.
|
||||
@@ -34,11 +31,9 @@ public final class GhostModeManager {
|
||||
get { defaults.bool(forKey: Keys.isEnabled) }
|
||||
set {
|
||||
defaults.set(newValue, forKey: Keys.isEnabled)
|
||||
if newValue && !isApplyingMutualExclusion {
|
||||
// Ghost Mode ON → disable Always Online
|
||||
isApplyingMutualExclusion = true
|
||||
if newValue {
|
||||
// Ghost Mode ON → disable Always Online so they don't coexist in UI
|
||||
MiscSettingsManager.shared.disableAlwaysOnlineForMutualExclusion()
|
||||
isApplyingMutualExclusion = false
|
||||
}
|
||||
notifySettingsChanged()
|
||||
}
|
||||
@@ -102,9 +97,11 @@ public final class GhostModeManager {
|
||||
}
|
||||
|
||||
/// Online status is hidden only when Ghost Mode is on AND Always Online is NOT active.
|
||||
/// Checks alwaysOnline raw value (not shouldAlwaysBeOnline) so ghost mode works
|
||||
/// even when the Misc master toggle is off.
|
||||
public var shouldHideOnlineStatus: Bool {
|
||||
guard isEnabled && hideOnlineStatus else { return false }
|
||||
return !MiscSettingsManager.shared.shouldAlwaysBeOnline
|
||||
return !MiscSettingsManager.shared.alwaysOnline
|
||||
}
|
||||
|
||||
public var shouldHideTypingIndicator: Bool {
|
||||
@@ -114,7 +111,7 @@ public final class GhostModeManager {
|
||||
/// Force offline only when Ghost Mode is on AND Always Online is NOT active.
|
||||
public var shouldForceOffline: Bool {
|
||||
guard isEnabled && forceOffline else { return false }
|
||||
return !MiscSettingsManager.shared.shouldAlwaysBeOnline
|
||||
return !MiscSettingsManager.shared.alwaysOnline
|
||||
}
|
||||
|
||||
/// Count of active features (e.g., "5/5")
|
||||
@@ -131,17 +128,6 @@ public final class GhostModeManager {
|
||||
/// Total number of features
|
||||
public static let totalFeatureCount = 5
|
||||
|
||||
// MARK: - Internal mutual exclusion (called by MiscSettingsManager)
|
||||
|
||||
/// Called by MiscSettingsManager when Always Online is turned on.
|
||||
/// Disables Ghost Mode without triggering mutual exclusion back.
|
||||
public func disableForMutualExclusion() {
|
||||
isApplyingMutualExclusion = true
|
||||
defaults.set(false, forKey: Keys.isEnabled)
|
||||
notifySettingsChanged()
|
||||
isApplyingMutualExclusion = false
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
private init() {
|
||||
|
||||
@@ -16,9 +16,6 @@ public final class MiscSettingsManager {
|
||||
|
||||
private let defaults = UserDefaults.standard
|
||||
|
||||
// Prevents recursive mutual-exclusion calls
|
||||
private var isApplyingMutualExclusion = false
|
||||
|
||||
// MARK: - Main Toggle
|
||||
|
||||
public var isEnabled: Bool {
|
||||
@@ -68,17 +65,12 @@ public final class MiscSettingsManager {
|
||||
}
|
||||
|
||||
/// Always appear as online.
|
||||
/// Enabling this automatically disables Ghost Mode (mutual exclusion).
|
||||
/// NOTE: Ghost Mode features dynamically yield to Always Online via their
|
||||
/// `shouldAlwaysBeOnline` check, so no permanent disabling is needed here.
|
||||
public var alwaysOnline: Bool {
|
||||
get { defaults.bool(forKey: Keys.alwaysOnline) }
|
||||
set {
|
||||
defaults.set(newValue, forKey: Keys.alwaysOnline)
|
||||
if newValue && !isApplyingMutualExclusion {
|
||||
// Always Online ON → disable Ghost Mode
|
||||
isApplyingMutualExclusion = true
|
||||
GhostModeManager.shared.disableForMutualExclusion()
|
||||
isApplyingMutualExclusion = false
|
||||
}
|
||||
notifySettingsChanged()
|
||||
}
|
||||
}
|
||||
@@ -122,7 +114,7 @@ public final class MiscSettingsManager {
|
||||
disableViewOnceAutoDelete = true
|
||||
bypassScreenshotProtection = true
|
||||
blockAds = true
|
||||
alwaysOnline = true // setter handles mutual exclusion
|
||||
alwaysOnline = true
|
||||
}
|
||||
|
||||
public func disableAll() {
|
||||
@@ -136,12 +128,10 @@ public final class MiscSettingsManager {
|
||||
// MARK: - Internal mutual exclusion (called by GhostModeManager)
|
||||
|
||||
/// Called by GhostModeManager when Ghost Mode is turned on.
|
||||
/// Disables Always Online without triggering mutual exclusion back.
|
||||
/// Disables Always Online so the two modes don't coexist in the UI.
|
||||
public func disableAlwaysOnlineForMutualExclusion() {
|
||||
isApplyingMutualExclusion = true
|
||||
defaults.set(false, forKey: Keys.alwaysOnline)
|
||||
notifySettingsChanged()
|
||||
isApplyingMutualExclusion = false
|
||||
}
|
||||
|
||||
// MARK: - Notification
|
||||
|
||||
@@ -365,14 +365,46 @@ public func enqueueMessages(account: Account, peerId: PeerId, messages: [Enqueue
|
||||
} else {
|
||||
signal = .single(messages.map { (false, $0) })
|
||||
}
|
||||
|
||||
let hasMedia = messages.contains { message in
|
||||
if case let .message(_, _, _, mediaReference, _, _, _, _, _, _) = message {
|
||||
return mediaReference != nil
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GHOSTGRAM: Send delay — write to Postbox immediately so the UI
|
||||
// clears the input field, then delay _only_ the return signal.
|
||||
// The actual network send delay is handled by scheduling: we add
|
||||
// OutgoingScheduleInfoMessageAttribute inside the transaction so
|
||||
// the message is stored as "scheduled" and Telegram server sends it
|
||||
// after the delay elapses. The message appears in Scheduled Messages
|
||||
// section for the duration of the delay.
|
||||
return signal
|
||||
|> mapToSignal { messages -> Signal<[MessageId?], NoError> in
|
||||
return account.postbox.transaction { transaction -> [MessageId?] in
|
||||
return enqueueMessages(transaction: transaction, account: account, peerId: peerId, messages: messages)
|
||||
var finalMessages = messages
|
||||
if SendDelayManager.shared.isEnabled {
|
||||
let delayInterval = hasMedia
|
||||
? SendDelayManager.mediaDelaySeconds
|
||||
: SendDelayManager.textDelaySeconds
|
||||
let scheduleTime = Int32(Date().timeIntervalSince1970) + Int32(delayInterval)
|
||||
finalMessages = messages.map { (transformed, msg) in
|
||||
let updatedMsg = msg.withUpdatedAttributes { attrs in
|
||||
var attrs = attrs
|
||||
attrs.removeAll(where: { $0 is OutgoingScheduleInfoMessageAttribute })
|
||||
attrs.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: scheduleTime, repeatPeriod: nil))
|
||||
return attrs
|
||||
}
|
||||
return (transformed, updatedMsg)
|
||||
}
|
||||
}
|
||||
return enqueueMessages(transaction: transaction, account: account, peerId: peerId, messages: finalMessages)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func enqueueMessagesToMultiplePeers(account: Account, peerIds: [PeerId], threadIds: [PeerId: Int64], messages: [EnqueueMessage]) -> Signal<[MessageId], NoError> {
|
||||
let signal: Signal<[(Bool, EnqueueMessage)], NoError>
|
||||
if let transformOutgoingMessageMedia = account.transformOutgoingMessageMedia {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import Foundation
|
||||
|
||||
/// SendDelayManager - delays outgoing messages by ~12 seconds to prevent
|
||||
/// online status from appearing after sending.
|
||||
///
|
||||
/// Delays are applied per-message at the enqueueMessages level.
|
||||
/// Media messages receive a slightly longer delay (~20 s) because upload
|
||||
/// time would otherwise reveal the send moment anyway.
|
||||
public final class SendDelayManager {
|
||||
|
||||
// MARK: - Singleton
|
||||
|
||||
public static let shared = SendDelayManager()
|
||||
|
||||
// MARK: - UserDefaults Keys
|
||||
|
||||
private enum Keys {
|
||||
static let isEnabled = "SendDelay.isEnabled"
|
||||
}
|
||||
|
||||
// MARK: - Storage
|
||||
|
||||
private let defaults = UserDefaults.standard
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
/// When true, all outgoing messages are delayed before being enqueued.
|
||||
public var isEnabled: Bool {
|
||||
get { defaults.bool(forKey: Keys.isEnabled) }
|
||||
set {
|
||||
defaults.set(newValue, forKey: Keys.isEnabled)
|
||||
notifySettingsChanged()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Delay constants
|
||||
|
||||
/// Base delay for text-only messages.
|
||||
public static let textDelaySeconds: Double = 12.0
|
||||
|
||||
/// Delay for messages that contain media attachments.
|
||||
public static let mediaDelaySeconds: Double = 20.0
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
private init() {}
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
public static let settingsChangedNotification = Notification.Name("SendDelaySettingsChanged")
|
||||
|
||||
private func notifySettingsChanged() {
|
||||
NotificationCenter.default.post(name: SendDelayManager.settingsChangedNotification, object: nil)
|
||||
}
|
||||
}
|
||||
@@ -4233,6 +4233,9 @@ func replayFinalState(
|
||||
if AntiDeleteManager.shared.isEnabled {
|
||||
let messageIds = transaction.messageIdsForGlobalIds(ids)
|
||||
for (index, messageId) in messageIds.enumerated() {
|
||||
// Skip scheduled/local/quick-reply messages — they get deleted when sent, not by the remote peer
|
||||
guard messageId.namespace == Namespaces.Message.Cloud else { continue }
|
||||
|
||||
if let message = transaction.getMessage(messageId) {
|
||||
let globalId = index < ids.count ? ids[index] : 0
|
||||
|
||||
@@ -4289,6 +4292,9 @@ func replayFinalState(
|
||||
if AntiDeleteManager.shared.isEnabled {
|
||||
let messageIds = transaction.messageIdsForGlobalIds(ids)
|
||||
for messageId in messageIds {
|
||||
// Skip scheduled/local/quick-reply messages — they get deleted when sent, not by the remote peer
|
||||
guard messageId.namespace == Namespaces.Message.Cloud else { continue }
|
||||
|
||||
// Mark as deleted for icon display
|
||||
AntiDeleteManager.shared.markAsDeleted(peerId: messageId.peerId.toInt64(), messageId: messageId.id)
|
||||
|
||||
@@ -4317,6 +4323,9 @@ func replayFinalState(
|
||||
// ANTI-DELETE: Archive channel messages with full content before deletion
|
||||
if AntiDeleteManager.shared.isEnabled {
|
||||
for messageId in ids {
|
||||
// Skip scheduled/local/quick-reply messages — they get deleted when sent, not by the remote peer
|
||||
guard messageId.namespace == Namespaces.Message.Cloud else { continue }
|
||||
|
||||
if let message = transaction.getMessage(messageId) {
|
||||
// Extract text content
|
||||
let textContent = message.text
|
||||
@@ -4370,6 +4379,9 @@ func replayFinalState(
|
||||
// ANTI-DELETE: Mark messages as deleted instead of removing them
|
||||
if AntiDeleteManager.shared.isEnabled {
|
||||
for messageId in ids {
|
||||
// Skip scheduled/local/quick-reply messages — they get deleted when sent, not by the remote peer
|
||||
guard messageId.namespace == Namespaces.Message.Cloud else { continue }
|
||||
|
||||
// Mark as deleted for icon display
|
||||
AntiDeleteManager.shared.markAsDeleted(peerId: messageId.peerId.toInt64(), messageId: messageId.id)
|
||||
|
||||
|
||||
@@ -82,16 +82,19 @@ private final class AccountPresenceManagerImpl {
|
||||
/// 2. Ghost Mode hide online status → skip update entirely (freeze last-seen)
|
||||
/// 3. Default app behaviour (wasOnline)
|
||||
private func refreshPresence() {
|
||||
let alwaysOnline = MiscSettingsManager.shared.shouldAlwaysBeOnline
|
||||
// Use raw alwaysOnline flag (not shouldAlwaysBeOnline) so it works independently
|
||||
// of the Misc master toggle. Ghost Mode's shouldHideOnlineStatus already checks
|
||||
// !MiscSettingsManager.shared.alwaysOnline internally.
|
||||
let alwaysOnline = MiscSettingsManager.shared.alwaysOnline
|
||||
let ghostHideOnline = GhostModeManager.shared.shouldHideOnlineStatus
|
||||
|
||||
if alwaysOnline {
|
||||
// Always Online wins — push online regardless of Ghost Mode
|
||||
sendPresenceUpdate(online: true)
|
||||
} else if ghostHideOnline {
|
||||
// Ghost Mode active, no Always Online — freeze presence (don't send anything)
|
||||
self.onlineTimer?.invalidate()
|
||||
self.onlineTimer = nil
|
||||
// Ghost Mode active: actively send offline so the server immediately
|
||||
// hides our last-seen instead of keeping the stale "online" status.
|
||||
sendPresenceUpdate(online: false)
|
||||
} else {
|
||||
// Normal mode — follow the app-level state
|
||||
sendPresenceUpdate(online: wasOnline)
|
||||
|
||||
@@ -27,7 +27,6 @@ import MediaEditor
|
||||
import AvatarBackground
|
||||
import LottieComponent
|
||||
import UndoUI
|
||||
import PremiumAlertController
|
||||
|
||||
public struct AvatarKeyboardInputData: Equatable {
|
||||
var emoji: EmojiPagerContentComponent
|
||||
|
||||
@@ -481,6 +481,10 @@ private func mapVisibility(_ visibility: ListViewItemNodeVisibility, boundsSize:
|
||||
}
|
||||
}
|
||||
|
||||
private func isDeletedBubbleMessage(_ message: Message) -> Bool {
|
||||
return AntiDeleteManager.shared.isMessageDeleted(peerId: message.id.peerId.toInt64(), messageId: message.id.id) || AntiDeleteManager.shared.isMessageDeleted(text: message.text)
|
||||
}
|
||||
|
||||
public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode {
|
||||
public class ContentContainer {
|
||||
public let contentMessageStableId: UInt32
|
||||
@@ -4368,6 +4372,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
}
|
||||
}
|
||||
|
||||
let deletedMessageAlpha = CGFloat(AntiDeleteManager.shared.deletedMessageDisplayAlpha)
|
||||
var deletedMessageStableIds = Set<UInt32>()
|
||||
for (message, _) in item.content {
|
||||
if isDeletedBubbleMessage(message) {
|
||||
deletedMessageStableIds.insert(message.stableId)
|
||||
}
|
||||
}
|
||||
|
||||
var incomingOffset: CGFloat = 0.0
|
||||
switch backgroundType {
|
||||
case .incoming:
|
||||
@@ -4512,10 +4524,31 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
}
|
||||
|
||||
contentContainer?.update(size: relativeFrame.size, contentOrigin: contentOrigin, selectionInsets: selectionInsets, index: index, presentationData: item.presentationData, graphics: graphics, backgroundType: backgroundType, presentationContext: item.controllerInteraction.presentationContext, mediaBox: item.context.account.postbox.mediaBox, messageSelection: itemSelection)
|
||||
|
||||
if let contentContainer = contentContainer {
|
||||
let containerAlpha: CGFloat = deletedMessageStableIds.contains(stableId) ? deletedMessageAlpha : 1.0
|
||||
if case .System = animation {
|
||||
animation.animator.updateAlpha(layer: contentContainer.sourceNode.contentNode.layer, alpha: containerAlpha, completion: nil)
|
||||
} else {
|
||||
contentContainer.sourceNode.contentNode.alpha = containerAlpha
|
||||
}
|
||||
}
|
||||
|
||||
index += 1
|
||||
}
|
||||
|
||||
let mainContainerAlpha: CGFloat
|
||||
if contentContainerNodeFrames.isEmpty, !deletedMessageStableIds.isEmpty {
|
||||
mainContainerAlpha = deletedMessageAlpha
|
||||
} else {
|
||||
mainContainerAlpha = 1.0
|
||||
}
|
||||
if case .System = animation {
|
||||
animation.animator.updateAlpha(layer: strongSelf.mainContextSourceNode.contentNode.layer, alpha: mainContainerAlpha, completion: nil)
|
||||
} else {
|
||||
strongSelf.mainContextSourceNode.contentNode.alpha = mainContainerAlpha
|
||||
}
|
||||
|
||||
if hasSelection {
|
||||
var currentMaskView: UIImageView?
|
||||
if let maskView = strongSelf.contentContainersWrapperNode.view.mask as? UIImageView {
|
||||
|
||||
@@ -29,6 +29,27 @@ public final class NavigationButtonComponent: Component {
|
||||
case more
|
||||
case icon(imageName: String)
|
||||
case proxy(status: ChatTitleProxyStatus)
|
||||
/// Liquid glass avatar button for account switching.
|
||||
/// peerId is used as a diff key; avatarImage is the rendered avatar.
|
||||
case avatar(peerId: String, avatarImage: UIImage?)
|
||||
|
||||
public static func ==(lhs: Content, rhs: Content) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (.text(lt, lb), .text(rt, rb)):
|
||||
return lt == rt && lb == rb
|
||||
case (.more, .more):
|
||||
return true
|
||||
case let (.icon(l), .icon(r)):
|
||||
return l == r
|
||||
case let (.proxy(l), .proxy(r)):
|
||||
return l == r
|
||||
case let (.avatar(lId, _), .avatar(rId, _)):
|
||||
// Re-render when peerId changes; image updates are handled by the view itself
|
||||
return lId == rId
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public let content: Content
|
||||
@@ -62,6 +83,12 @@ public final class NavigationButtonComponent: Component {
|
||||
|
||||
private var moreButton: MoreHeaderButton?
|
||||
|
||||
// MARK: - Liquid Glass Avatar
|
||||
private var avatarContainerView: UIView?
|
||||
private var avatarBlurView: UIVisualEffectView?
|
||||
private var avatarImageView: UIImageView?
|
||||
private var avatarBorderLayer: CAShapeLayer?
|
||||
|
||||
private var component: NavigationButtonComponent?
|
||||
private var theme: PresentationTheme?
|
||||
|
||||
@@ -74,19 +101,23 @@ public final class NavigationButtonComponent: Component {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if highlighted {
|
||||
self.textView?.alpha = 0.6
|
||||
self.proxyNode?.alpha = 0.6
|
||||
self.iconView?.alpha = 0.6
|
||||
} else {
|
||||
self.textView?.alpha = 1.0
|
||||
self.textView?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2)
|
||||
|
||||
self.proxyNode?.alpha = 1.0
|
||||
self.proxyNode?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2)
|
||||
|
||||
self.iconView?.alpha = 1.0
|
||||
self.iconView?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2)
|
||||
let alpha: CGFloat = highlighted ? 0.55 : 1.0
|
||||
self.textView?.alpha = alpha
|
||||
self.proxyNode?.alpha = alpha
|
||||
self.iconView?.alpha = alpha
|
||||
self.avatarContainerView?.alpha = alpha
|
||||
if !highlighted {
|
||||
let animateAlpha = { (layer: CALayer?) in
|
||||
let anim = CABasicAnimation(keyPath: "opacity")
|
||||
anim.fromValue = 0.55
|
||||
anim.toValue = 1.0
|
||||
anim.duration = 0.2
|
||||
layer?.add(anim, forKey: "opacity")
|
||||
}
|
||||
animateAlpha(self.textView?.layer)
|
||||
animateAlpha(self.proxyNode?.layer)
|
||||
animateAlpha(self.iconView?.layer)
|
||||
animateAlpha(self.avatarContainerView?.layer)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,6 +130,51 @@ public final class NavigationButtonComponent: Component {
|
||||
self.component?.pressed(self)
|
||||
}
|
||||
|
||||
// MARK: - Liquid glass avatar setup
|
||||
|
||||
private func setupAvatarViewsIfNeeded() {
|
||||
guard avatarContainerView == nil else { return }
|
||||
|
||||
// Container holds blur + image
|
||||
let container = UIView()
|
||||
container.isUserInteractionEnabled = false
|
||||
container.clipsToBounds = true
|
||||
|
||||
// Blur background — liquid glass effect
|
||||
let blurEffect = UIBlurEffect(style: .systemUltraThinMaterial)
|
||||
let blurView = UIVisualEffectView(effect: blurEffect)
|
||||
blurView.isUserInteractionEnabled = false
|
||||
container.addSubview(blurView)
|
||||
|
||||
// Avatar image on top of blur
|
||||
let imageView = UIImageView()
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.isUserInteractionEnabled = false
|
||||
imageView.clipsToBounds = true
|
||||
container.addSubview(imageView)
|
||||
|
||||
self.addSubview(container)
|
||||
self.avatarContainerView = container
|
||||
self.avatarBlurView = blurView
|
||||
self.avatarImageView = imageView
|
||||
|
||||
// Subtle glass ring border
|
||||
let borderLayer = CAShapeLayer()
|
||||
borderLayer.fillColor = UIColor.clear.cgColor
|
||||
borderLayer.strokeColor = UIColor.white.withAlphaComponent(0.22).cgColor
|
||||
borderLayer.lineWidth = 1.5
|
||||
container.layer.addSublayer(borderLayer)
|
||||
self.avatarBorderLayer = borderLayer
|
||||
}
|
||||
|
||||
private func removeAvatarViews() {
|
||||
avatarContainerView?.removeFromSuperview()
|
||||
avatarContainerView = nil
|
||||
avatarBlurView = nil
|
||||
avatarImageView = nil
|
||||
avatarBorderLayer = nil
|
||||
}
|
||||
|
||||
func update(component: NavigationButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<NavigationButtonComponentEnvironment>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
|
||||
@@ -113,6 +189,7 @@ public final class NavigationButtonComponent: Component {
|
||||
var imageName: String?
|
||||
var proxyStatus: ChatTitleProxyStatus?
|
||||
var isMore: Bool = false
|
||||
var avatarContent: (peerId: String, image: UIImage?)? = nil
|
||||
|
||||
switch component.content {
|
||||
case let .text(title, isBold):
|
||||
@@ -123,10 +200,13 @@ public final class NavigationButtonComponent: Component {
|
||||
imageName = imageNameValue
|
||||
case let .proxy(status):
|
||||
proxyStatus = status
|
||||
case let .avatar(peerId, image):
|
||||
avatarContent = (peerId, image)
|
||||
}
|
||||
|
||||
var size = CGSize(width: 0.0, height: availableSize.height)
|
||||
|
||||
// MARK: Text
|
||||
if let textString = textString {
|
||||
let textView: ImmediateTextView
|
||||
if let current = self.textView {
|
||||
@@ -144,11 +224,13 @@ public final class NavigationButtonComponent: Component {
|
||||
size.width = max(44.0, textSize.width + textInset * 2.0)
|
||||
|
||||
textView.frame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: floor((availableSize.height - textSize.height) / 2.0)), size: textSize)
|
||||
removeAvatarViews()
|
||||
} else if let textView = self.textView {
|
||||
self.textView = nil
|
||||
textView.removeFromSuperview()
|
||||
}
|
||||
|
||||
// MARK: Icon
|
||||
if let imageName = imageName {
|
||||
let iconView: UIImageView
|
||||
if let current = self.iconView {
|
||||
@@ -166,15 +248,16 @@ public final class NavigationButtonComponent: Component {
|
||||
|
||||
if let iconSize = iconView.image?.size {
|
||||
size.width = 44.0
|
||||
|
||||
iconView.frame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((availableSize.height - iconSize.height) / 2.0)), size: iconSize)
|
||||
}
|
||||
removeAvatarViews()
|
||||
} else if let iconView = self.iconView {
|
||||
self.iconView = nil
|
||||
iconView.removeFromSuperview()
|
||||
self.iconImageName = nil
|
||||
}
|
||||
|
||||
// MARK: Proxy
|
||||
if let proxyStatus = proxyStatus {
|
||||
let proxyNode: ChatTitleProxyNode
|
||||
if let current = self.proxyNode {
|
||||
@@ -191,13 +274,14 @@ public final class NavigationButtonComponent: Component {
|
||||
|
||||
proxyNode.theme = theme
|
||||
proxyNode.status = proxyStatus
|
||||
|
||||
proxyNode.frame = CGRect(origin: CGPoint(x: floor((size.width - proxySize.width) / 2.0), y: floor((availableSize.height - proxySize.height) / 2.0)), size: proxySize)
|
||||
removeAvatarViews()
|
||||
} else if let proxyNode = self.proxyNode {
|
||||
self.proxyNode = nil
|
||||
proxyNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
// MARK: More
|
||||
if isMore {
|
||||
let moreButton: MoreHeaderButton
|
||||
if let current = self.moreButton, !themeUpdated {
|
||||
@@ -233,13 +317,52 @@ public final class NavigationButtonComponent: Component {
|
||||
size.width = 44.0
|
||||
|
||||
moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: theme.rootController.navigationBar.buttonColor)))
|
||||
|
||||
moreButton.frame = CGRect(origin: CGPoint(x: floor((size.width - buttonSize.width) / 2.0), y: floor((size.height - buttonSize.height) / 2.0)), size: buttonSize)
|
||||
removeAvatarViews()
|
||||
} else if let moreButton = self.moreButton {
|
||||
self.moreButton = nil
|
||||
moreButton.removeFromSupernode()
|
||||
}
|
||||
|
||||
// MARK: Liquid Glass Avatar
|
||||
if let (_, image) = avatarContent {
|
||||
setupAvatarViewsIfNeeded()
|
||||
|
||||
let avatarDiameter: CGFloat = 28.0
|
||||
size.width = 44.0
|
||||
|
||||
let containerRect = CGRect(
|
||||
x: floor((size.width - avatarDiameter) / 2.0),
|
||||
y: floor((availableSize.height - avatarDiameter) / 2.0),
|
||||
width: avatarDiameter,
|
||||
height: avatarDiameter
|
||||
)
|
||||
|
||||
avatarContainerView?.frame = containerRect
|
||||
avatarContainerView?.layer.cornerRadius = avatarDiameter / 2.0
|
||||
|
||||
avatarBlurView?.frame = CGRect(origin: .zero, size: containerRect.size)
|
||||
|
||||
if let image = image {
|
||||
avatarImageView?.image = image
|
||||
avatarImageView?.frame = CGRect(origin: .zero, size: containerRect.size)
|
||||
avatarImageView?.backgroundColor = nil
|
||||
} else {
|
||||
avatarImageView?.image = nil
|
||||
// Fallback: solid tinted background when no photo
|
||||
avatarImageView?.frame = CGRect(origin: .zero, size: containerRect.size)
|
||||
avatarImageView?.backgroundColor = theme.list.itemAccentColor.withAlphaComponent(0.35)
|
||||
}
|
||||
|
||||
// Update border ring path
|
||||
let borderPath = UIBezierPath(roundedRect: CGRect(origin: .zero, size: containerRect.size).insetBy(dx: 0.75, dy: 0.75), cornerRadius: avatarDiameter / 2.0)
|
||||
avatarBorderLayer?.path = borderPath.cgPath
|
||||
avatarBorderLayer?.frame = CGRect(origin: .zero, size: containerRect.size)
|
||||
|
||||
} else if avatarContent == nil && avatarContainerView != nil {
|
||||
removeAvatarViews()
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ swift_library(
|
||||
module_name = "GiftViewScreen",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
], exclude = [
|
||||
"Sources/TableComponent.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
|
||||
@@ -133,7 +133,7 @@ public func giftOfferAlertController(
|
||||
HStack(items, spacing: 4.0)
|
||||
)
|
||||
|
||||
tableItems.append(.init(
|
||||
tableItems.append(TableComponent.Item(
|
||||
id: id,
|
||||
title: title,
|
||||
hasBackground: false,
|
||||
@@ -180,12 +180,12 @@ public func giftOfferAlertController(
|
||||
AlertTextComponent(content: .plain(text))
|
||||
)
|
||||
))
|
||||
content.append(AnyComponentWithIdentity(
|
||||
let tableComponent = AnyComponent(AlertTableComponent(items: tableItems))
|
||||
let tableEntry = AnyComponentWithIdentity<AlertComponentEnvironment>(
|
||||
id: "table",
|
||||
component: AnyComponent(
|
||||
AlertTableComponent(items: tableItems)
|
||||
)
|
||||
))
|
||||
component: tableComponent
|
||||
)
|
||||
content.append(tableEntry)
|
||||
|
||||
if let valueAmount = gift.valueUsdAmount {
|
||||
let resaleConfiguration = StarsSubscriptionConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
|
||||
|
||||
@@ -118,7 +118,7 @@ public func giftTransferAlertController(
|
||||
HStack(items, spacing: 4.0)
|
||||
)
|
||||
|
||||
tableItems.append(.init(
|
||||
tableItems.append(TableComponent.Item(
|
||||
id: id,
|
||||
title: title,
|
||||
hasBackground: false,
|
||||
@@ -165,12 +165,12 @@ public func giftTransferAlertController(
|
||||
AlertTextComponent(content: .plain(text))
|
||||
)
|
||||
))
|
||||
content.append(AnyComponentWithIdentity(
|
||||
let tableComponent = AnyComponent(AlertTableComponent(items: tableItems))
|
||||
let tableEntry = AnyComponentWithIdentity<AlertComponentEnvironment>(
|
||||
id: "table",
|
||||
component: AnyComponent(
|
||||
AlertTableComponent(items: tableItems)
|
||||
)
|
||||
))
|
||||
component: tableComponent
|
||||
)
|
||||
content.append(tableEntry)
|
||||
|
||||
let alertController = ChatMessagePaymentAlertController(
|
||||
context: context,
|
||||
|
||||
@@ -8,7 +8,7 @@ import ChatPresentationInterfaceState
|
||||
import AsyncDisplayKit
|
||||
import AccountContext
|
||||
|
||||
open class ChatTitleAccessoryPanelNode: ASDisplayNode {
|
||||
open class LegacyChatTitleAccessoryPanelNode: ASDisplayNode {
|
||||
public typealias LayoutResult = ChatControllerCustomNavigationPanelNodeLayoutResult
|
||||
|
||||
open var interfaceInteraction: ChatPanelInterfaceInteraction?
|
||||
@@ -19,11 +19,11 @@ open class ChatTitleAccessoryPanelNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
public final class LegacyChatHeaderPanelComponent: Component {
|
||||
public let panelNode: ChatTitleAccessoryPanelNode
|
||||
public let panelNode: LegacyChatTitleAccessoryPanelNode
|
||||
public let interfaceState: ChatPresentationInterfaceState
|
||||
|
||||
public init(
|
||||
panelNode: ChatTitleAccessoryPanelNode,
|
||||
panelNode: LegacyChatTitleAccessoryPanelNode,
|
||||
interfaceState: ChatPresentationInterfaceState
|
||||
) {
|
||||
self.panelNode = panelNode
|
||||
|
||||
@@ -232,20 +232,34 @@ final class AffiliateProgramSetupScreenComponent: Component {
|
||||
)
|
||||
))
|
||||
|
||||
let tableItems: [TableComponent.Item] = [
|
||||
TableComponent.Item(id: 0, title: environment.strings.AffiliateSetup_AlertApply_SectionCommission, component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: commissionTitle, font: Font.regular(15.0), textColor: environment.theme.actionSheet.primaryTextColor))
|
||||
))),
|
||||
TableComponent.Item(id: 1, title: environment.strings.AffiliateSetup_AlertApply_SectionDuration, component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: durationTitle, font: Font.regular(15.0), textColor: environment.theme.actionSheet.primaryTextColor))
|
||||
)))
|
||||
]
|
||||
content.append(AnyComponentWithIdentity(
|
||||
let textColor = environment.theme.actionSheet.primaryTextColor
|
||||
let commissionItemText = NSAttributedString(
|
||||
string: commissionTitle,
|
||||
font: Font.regular(15.0),
|
||||
textColor: textColor
|
||||
)
|
||||
let durationItemText = NSAttributedString(
|
||||
string: durationTitle,
|
||||
font: Font.regular(15.0),
|
||||
textColor: textColor
|
||||
)
|
||||
let commissionItem = TableComponent.Item(
|
||||
id: 0,
|
||||
title: environment.strings.AffiliateSetup_AlertApply_SectionCommission,
|
||||
component: AnyComponent(MultilineTextComponent(text: .plain(commissionItemText)))
|
||||
)
|
||||
let durationItem = TableComponent.Item(
|
||||
id: 1,
|
||||
title: environment.strings.AffiliateSetup_AlertApply_SectionDuration,
|
||||
component: AnyComponent(MultilineTextComponent(text: .plain(durationItemText)))
|
||||
)
|
||||
let tableItems: [TableComponent.Item] = [commissionItem, durationItem]
|
||||
let tableComponent = AnyComponent(AlertTableComponent(items: tableItems))
|
||||
let tableEntry = AnyComponentWithIdentity<AlertComponentEnvironment>(
|
||||
id: "table",
|
||||
component: AnyComponent(
|
||||
AlertTableComponent(items: tableItems)
|
||||
)
|
||||
))
|
||||
component: tableComponent
|
||||
)
|
||||
content.append(tableEntry)
|
||||
|
||||
let alertController = AlertScreen(
|
||||
context: component.context,
|
||||
|
||||
@@ -6,7 +6,7 @@ import TelegramPresentationData
|
||||
import MultilineTextComponent
|
||||
import AlertComponent
|
||||
|
||||
final class TableComponent: CombinedComponent {
|
||||
final class AffiliateTableComponent: CombinedComponent {
|
||||
class Item: Equatable {
|
||||
public let id: AnyHashable
|
||||
public let title: String
|
||||
@@ -45,7 +45,7 @@ final class TableComponent: CombinedComponent {
|
||||
self.items = items
|
||||
}
|
||||
|
||||
public static func ==(lhs: TableComponent, rhs: TableComponent) -> Bool {
|
||||
public static func ==(lhs: AffiliateTableComponent, rhs: AffiliateTableComponent) -> Bool {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
@@ -228,9 +228,9 @@ private final class TableAlertContentComponent: CombinedComponent {
|
||||
let theme: PresentationTheme
|
||||
let title: String
|
||||
let text: String
|
||||
let table: TableComponent
|
||||
let table: AffiliateTableComponent
|
||||
|
||||
init(theme: PresentationTheme, title: String, text: String, table: TableComponent) {
|
||||
init(theme: PresentationTheme, title: String, text: String, table: AffiliateTableComponent) {
|
||||
self.theme = theme
|
||||
self.title = title
|
||||
self.text = text
|
||||
@@ -256,7 +256,7 @@ private final class TableAlertContentComponent: CombinedComponent {
|
||||
public static var body: Body {
|
||||
let title = Child(MultilineTextComponent.self)
|
||||
let text = Child(MultilineTextComponent.self)
|
||||
let table = Child(TableComponent.self)
|
||||
let table = Child(AffiliateTableComponent.self)
|
||||
|
||||
return { context in
|
||||
let title = title.update(
|
||||
@@ -318,17 +318,3 @@ private final class TableAlertContentComponent: CombinedComponent {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tableAlert(theme: PresentationTheme, title: String, text: String, table: TableComponent, actions: [ComponentAlertAction]) -> ViewController {
|
||||
return componentAlertController(
|
||||
theme: AlertControllerTheme(presentationTheme: theme, fontSize: .regular),
|
||||
content: AnyComponent(TableAlertContentComponent(
|
||||
theme: theme,
|
||||
title: title,
|
||||
text: text,
|
||||
table: table
|
||||
)),
|
||||
actions: actions,
|
||||
actionLayout: .horizontal
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ import TelegramPresentationData
|
||||
import ProgressNavigationButtonNode
|
||||
import AccountContext
|
||||
import SearchUI
|
||||
import ChatListUI
|
||||
import func ChatListUI.chatListFilterItems
|
||||
import enum ChatListUI.ChatListContainerNodeFilter
|
||||
import CounterControllerTitleView
|
||||
import ChatListFilterTabContainerNode
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ public final class BirthdayPickerComponent: Component {
|
||||
|
||||
private let calendar = Calendar(identifier: .gregorian)
|
||||
private var value = TelegramBirthday(day: 1, month: 1, year: nil)
|
||||
private var minYear: Int32 = 1900
|
||||
private var minYear: Int32 = 1
|
||||
private let maxYear: Int32
|
||||
|
||||
override init(frame: CGRect) {
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.6581 5.56807C9.26054 4.9489 10.0877 4.59961 10.9516 4.59961H18.4004C20.1677 4.59961 21.6004 6.0323 21.6004 7.79961V16.1996C21.6004 17.9669 20.1677 19.3996 18.4004 19.3996H10.9516C10.0877 19.3996 9.26054 19.0503 8.6581 18.4311L3.486 13.1154C2.88168 12.4943 2.88168 11.5049 3.486 10.8838L8.6581 5.56807Z" stroke="black" stroke-width="1.66"/>
|
||||
<path d="M11.5996 9.2002L17.1996 14.8002" stroke="black" stroke-width="1.66" stroke-linecap="round"/>
|
||||
<path d="M11.5996 14.7998L17.1996 9.1998" stroke="black" stroke-width="1.66" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 659 B |
@@ -1,8 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="9.2" stroke="black" stroke-width="1.66"/>
|
||||
<ellipse cx="12" cy="12" rx="5.2" ry="9.2" stroke="black" stroke-width="1.328"/>
|
||||
<path d="M12 2.8V21.2" stroke="black" stroke-width="1.328"/>
|
||||
<path d="M2.8 12L21.2 12" stroke="black" stroke-width="1.328"/>
|
||||
<path d="M4.4 17.6C4.4 17.6 6.75325 16 12 16C17.2468 16 19.6 17.6 19.6 17.6" stroke="black" stroke-width="1.328"/>
|
||||
<path d="M19.6 6.4C19.6 6.4 17.2468 8 12 8C6.75325 8 4.4 6.4 4.4 6.4" stroke="black" stroke-width="1.328"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 601 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.0626 21.9306H12.9374C13.652 21.9306 14.1996 21.494 14.3666 20.8065L14.7657 19.0693L15.0627 18.9672L16.5755 19.8961C17.1787 20.277 17.8748 20.1841 18.3852 19.6732L19.6846 18.3819C20.195 17.871 20.2879 17.165 19.9073 16.5704L18.9607 15.0656L19.0721 14.7868L20.8076 14.3781C21.4851 14.2109 21.9306 13.6535 21.9306 12.9475V11.1082C21.9306 10.4022 21.4944 9.84484 20.8076 9.67761L19.0906 9.2596L18.9699 8.96231L19.9166 7.45739C20.2971 6.86287 20.2043 6.16613 19.6939 5.64595L18.3945 4.34539C17.8934 3.84375 17.1973 3.75086 16.594 4.12244L15.0812 5.05138L14.7657 4.93066L14.3666 3.19348C14.1996 2.50605 13.652 2.06944 12.9374 2.06944H11.0626C10.348 2.06944 9.80046 2.50605 9.63335 3.19348L9.22503 4.93066L8.90949 5.05138L7.40598 4.12244C6.80272 3.75086 6.09735 3.84375 5.5962 4.34539L4.30614 5.64595C3.79569 6.16613 3.6936 6.86287 4.0834 7.45739L5.02077 8.96231L4.90939 9.2596L3.19243 9.67761C2.50565 9.84484 2.06945 10.4022 2.06945 11.1082V12.9475C2.06945 13.6535 2.51493 14.2109 3.19243 14.3781L4.92794 14.7868L5.03005 15.0656L4.09268 16.5704C3.70289 17.165 3.80498 17.871 4.31542 18.3819L5.60548 19.6732C6.1159 20.1841 6.82127 20.277 7.42453 19.8961L8.92804 18.9672L9.22503 19.0693L9.63335 20.8065C9.80046 21.494 10.348 21.9306 11.0626 21.9306ZM11.2111 20.4814C11.0534 20.4814 10.9698 20.4164 10.942 20.2677L10.3851 17.9639C9.81901 17.8246 9.28997 17.6016 8.89093 17.3508L6.86766 18.5956C6.75627 18.6792 6.63567 18.6606 6.52428 18.5492L5.42915 17.453C5.32704 17.3508 5.31776 17.2393 5.39198 17.1092L6.63567 15.1027C6.42217 14.7126 6.1809 14.1831 6.03241 13.6164L3.73073 13.0683C3.58223 13.0404 3.51727 12.9568 3.51727 12.7989V11.2475C3.51727 11.0803 3.57295 11.006 3.73073 10.9781L6.02313 10.4208C6.17163 9.81694 6.45 9.26888 6.61711 8.92515L5.3827 6.91859C5.29921 6.77926 5.30849 6.66775 5.41059 6.55631L6.515 5.47873C6.62639 5.36722 6.72844 5.34867 6.86766 5.43228L8.87232 6.6492C9.27142 6.42625 9.83757 6.19402 10.3944 6.03607L10.942 3.73228C10.9698 3.58365 11.0534 3.51862 11.2111 3.51862H12.7889C12.9466 3.51862 13.0302 3.58365 13.0487 3.73228L13.6149 6.05469C14.1903 6.2033 14.6915 6.43553 15.1091 6.65848L17.1231 5.43228C17.2716 5.34867 17.3643 5.36722 17.485 5.47873L18.5801 6.55631C18.6915 6.66775 18.6915 6.77926 18.608 6.91859L17.3737 8.92515C17.55 9.26888 17.8191 9.81694 17.9676 10.4208L20.2693 10.9781C20.4178 11.006 20.4827 11.0803 20.4827 11.2475V12.7989C20.4827 12.9568 20.4085 13.0404 20.2693 13.0683L17.9583 13.6164C17.8098 14.1831 17.5778 14.7126 17.3551 15.1027L18.5987 17.1092C18.673 17.2393 18.673 17.3508 18.5616 17.453L17.4757 18.5492C17.3551 18.6606 17.2437 18.6792 17.1231 18.5956L15.0998 17.3508C14.7007 17.6016 14.181 17.8246 13.6149 17.9639L13.0487 20.2677C13.0302 20.4164 12.9466 20.4814 12.7889 20.4814H11.2111ZM12 15.5486C13.9397 15.5486 15.536 13.9508 15.536 12C15.536 10.0678 13.9397 8.46997 12 8.46997C10.0603 8.46997 8.45472 10.0678 8.45472 12C8.45472 13.9415 10.051 15.5486 12 15.5486ZM12 14.1087C10.8491 14.1087 9.90251 13.1612 9.90251 12C9.90251 10.8574 10.8491 9.90984 12 9.90984C13.1323 9.90984 14.0789 10.8574 14.0789 12C14.0789 13.1519 13.1323 14.1087 12 14.1087Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.2 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M22 4.515C12.3433 4.515 4.515 12.3433 4.515 22C4.515 31.6567 12.3433 39.485 22 39.485C31.6567 39.485 39.485 31.6567 39.485 22C39.485 12.3433 31.6567 4.515 22 4.515ZM1.485 22C1.485 10.6699 10.6699 1.485 22 1.485C33.3301 1.485 42.515 10.6699 42.515 22C42.515 33.3301 33.3301 42.515 22 42.515C10.6699 42.515 1.485 33.3301 1.485 22ZM21.9986 13.152C22.8353 13.152 23.5136 13.8303 23.5136 14.667V20.4854H29.3319C30.1686 20.4854 30.8469 21.1636 30.8469 22.0004C30.8469 22.8371 30.1686 23.5154 29.3319 23.5154H23.5136V29.3337C23.5136 30.1704 22.8353 30.8487 21.9986 30.8487C21.1619 30.8487 20.4836 30.1704 20.4836 29.3337V23.5154H14.6652C13.8285 23.5154 13.1502 22.8371 13.1502 22.0004C13.1502 21.1636 13.8285 20.4854 14.6652 20.4854H20.4836V14.667C20.4836 13.8303 21.1619 13.152 21.9986 13.152Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 956 B |
@@ -692,6 +692,44 @@ extension ChatControllerImpl {
|
||||
|
||||
self.displayNode = ChatControllerNode(context: self.context, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, subject: self.subject, controllerInteraction: self.controllerInteraction!, chatPresentationInterfaceState: self.presentationInterfaceState, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings, navigationBar: self.navigationBar, statusBar: self.statusBar, backgroundNode: self.chatBackgroundNode, controller: self)
|
||||
|
||||
// Seed the title pill synchronously from whatever data is available at this point,
|
||||
// so the header shows the peer name immediately instead of staying blank until
|
||||
// the first async contentDataUpdated() fires.
|
||||
let initialTitleContent: ChatTitleContent? = self.contentData?.state.chatTitleContent
|
||||
?? self.presentationInterfaceState.renderedPeer.flatMap { renderedPeer -> ChatTitleContent? in
|
||||
guard let peer = renderedPeer.peer else { return nil }
|
||||
let peerData = ChatTitleContent.PeerData(
|
||||
peerId: renderedPeer.peerId,
|
||||
peer: peer,
|
||||
isContact: false,
|
||||
isSavedMessages: peer.id == self.context.account.peerId,
|
||||
notificationSettings: nil,
|
||||
peerPresences: [:],
|
||||
cachedData: nil
|
||||
)
|
||||
return .peer(
|
||||
peerView: peerData,
|
||||
customTitle: nil,
|
||||
customSubtitle: nil,
|
||||
onlineMemberCount: (nil, nil),
|
||||
isScheduledMessages: false,
|
||||
isMuted: nil,
|
||||
customMessageCount: nil,
|
||||
isEnabled: true
|
||||
)
|
||||
}
|
||||
if let initialTitleContent {
|
||||
self.chatTitleView?.update(
|
||||
context: self.context,
|
||||
theme: self.presentationData.theme,
|
||||
strings: self.presentationData.strings,
|
||||
dateTimeFormat: self.presentationData.dateTimeFormat,
|
||||
nameDisplayOrder: self.presentationData.nameDisplayOrder,
|
||||
content: initialTitleContent,
|
||||
transition: .immediate
|
||||
)
|
||||
}
|
||||
|
||||
if let currentItem = self.globalControlPanelsContext?.tempVoicePlaylistCurrentItem {
|
||||
self.chatDisplayNode.historyNode.voicePlaylistItemChanged(nil, currentItem)
|
||||
}
|
||||
|
||||
@@ -146,7 +146,6 @@ import ChatTextInputPanelNode
|
||||
import ChatInputAccessoryPanel
|
||||
import GlobalControlPanelsContext
|
||||
import ChatSearchNavigationContentNode
|
||||
import ChatAgeRestrictionAlertController
|
||||
|
||||
public final class ChatControllerOverlayPresentationData {
|
||||
public let expandData: (ASDisplayNode?, () -> Void)
|
||||
@@ -830,7 +829,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
return true
|
||||
}
|
||||
|
||||
let controllerInteraction = ChatControllerInteraction(openMessage: { [weak self] message, params in
|
||||
let openMessage: (Message, OpenMessageParams) -> Bool = { [weak self] message, params in
|
||||
guard let self, self.isNodeLoaded, let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(message.id) else {
|
||||
return false
|
||||
}
|
||||
@@ -1566,7 +1565,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
self.controllerInteraction?.isOpeningMediaSignal = openChatMessageParams.blockInteraction.get()
|
||||
|
||||
return context.sharedContext.openChatMessage(openChatMessageParams)
|
||||
}, openPeer: { [weak self] peer, navigation, fromMessage, source in
|
||||
}
|
||||
|
||||
let openPeer: (EnginePeer, ChatControllerInteractionNavigateToPeer, MessageReference?, ChatControllerInteraction.OpenPeerSource) -> Void = { [weak self] peer, navigation, fromMessage, source in
|
||||
var expandAvatar = false
|
||||
if case let .groupParticipant(storyStats, avatarHeaderNode) = source {
|
||||
if let storyStats, storyStats.totalCount != 0, let avatarHeaderNode = avatarHeaderNode as? ChatMessageAvatarHeaderNodeImpl {
|
||||
@@ -1581,20 +1582,28 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
fromReactionMessageId = fromMessage?.id
|
||||
}
|
||||
self?.openPeer(peer: peer, navigation: navigation, fromMessage: fromMessage, fromReactionMessageId: fromReactionMessageId, expandAvatar: expandAvatar)
|
||||
}, openPeerMention: { [weak self] name, progress in
|
||||
}
|
||||
|
||||
let openPeerMention: (String, Promise<Bool>?) -> Void = { [weak self] name, progress in
|
||||
self?.openPeerMention(name, progress: progress)
|
||||
}, openMessageContextMenu: { [weak self] message, selectAll, node, frame, anyRecognizer, location in
|
||||
}
|
||||
|
||||
let openMessageContextMenu: (Message, Bool, ASDisplayNode, CGRect, UIGestureRecognizer?, CGPoint?) -> Void = { [weak self] message, selectAll, node, frame, anyRecognizer, location in
|
||||
guard let self, self.isNodeLoaded else {
|
||||
return
|
||||
}
|
||||
self.openMessageContextMenu(message: message, selectAll: selectAll, node: node, frame: frame, anyRecognizer: anyRecognizer, location: location)
|
||||
}, openMessageReactionContextMenu: { [weak self] message, sourceView, gesture, value in
|
||||
}
|
||||
|
||||
let openMessageReactionContextMenu: (Message, ContextExtractedContentContainingView, ContextGesture?, MessageReaction.Reaction) -> Void = { [weak self] message, sourceView, gesture, value in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.openMessageReactionContextMenu(message: message, sourceView: sourceView, gesture: gesture, value: value)
|
||||
}, updateMessageReaction: { [weak self] initialMessage, reaction, force, sourceView in
|
||||
}
|
||||
|
||||
let updateMessageReaction: (Message, ChatControllerInteractionReaction, Bool, ContextExtractedContentContainingView?) -> Void = { [weak self] initialMessage, reaction, force, sourceView in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
@@ -2062,7 +2071,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
}
|
||||
})
|
||||
}, activateMessagePinch: { [weak self] sourceNode in
|
||||
}
|
||||
|
||||
let controllerInteraction = ChatControllerInteraction(
|
||||
openMessage: openMessage,
|
||||
openPeer: openPeer,
|
||||
openPeerMention: openPeerMention,
|
||||
openMessageContextMenu: openMessageContextMenu,
|
||||
openMessageReactionContextMenu: openMessageReactionContextMenu,
|
||||
updateMessageReaction: updateMessageReaction,
|
||||
activateMessagePinch: { [weak self] sourceNode in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -893,7 +893,8 @@ extension ChatControllerImpl {
|
||||
peerVerification = cachedChannelData.verification
|
||||
}
|
||||
}
|
||||
copyProtectionEnabled = peer.isCopyProtectionEnabled
|
||||
// GHOSTGRAM: Bypass copy protection if enabled in Misc settings
|
||||
copyProtectionEnabled = MiscSettingsManager.shared.shouldBypassCopyProtection ? false : peer.isCopyProtectionEnabled
|
||||
if let cachedGroupData = peerView.cachedData as? CachedGroupData {
|
||||
if !cachedGroupData.botInfos.isEmpty {
|
||||
hasBots = true
|
||||
@@ -1371,7 +1372,8 @@ extension ChatControllerImpl {
|
||||
var alwaysShowGiftButton = false
|
||||
var disallowedGifts: TelegramDisallowedGifts?
|
||||
if let peer = peerView.peers[peerView.peerId] {
|
||||
copyProtectionEnabled = peer.isCopyProtectionEnabled
|
||||
// GHOSTGRAM: Bypass copy protection if enabled in Misc settings
|
||||
copyProtectionEnabled = MiscSettingsManager.shared.shouldBypassCopyProtection ? false : peer.isCopyProtectionEnabled
|
||||
if let cachedData = peerView.cachedData as? CachedUserData {
|
||||
contactStatus = ChatContactStatus(canAddContact: !peerView.peerIsContact, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: nil, managingBot: managingBot)
|
||||
if case let .known(value) = cachedData.businessIntro {
|
||||
|
||||
@@ -1129,7 +1129,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
let isSecret = self.chatPresentationInterfaceState.copyProtectionEnabled || self.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat || self.chatLocation.peerId?.isVerificationCodes == true
|
||||
// GHOSTGRAM: Bypass screenshot protection if enabled in Misc settings
|
||||
let effectiveCopyProtection = MiscSettingsManager.shared.shouldBypassScreenshotProtection ? false : self.chatPresentationInterfaceState.copyProtectionEnabled
|
||||
let isSecret = effectiveCopyProtection || self.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat || self.chatLocation.peerId?.isVerificationCodes == true
|
||||
if self.historyNodeContainer.isSecret != isSecret {
|
||||
self.historyNodeContainer.isSecret = isSecret
|
||||
setLayerDisableScreenshots(self.titleAccessoryPanelContainer.layer, isSecret)
|
||||
@@ -4600,12 +4602,10 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
self.setupSendActionOnViewUpdate({ [weak self] in
|
||||
guard let self, let textInputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode else {
|
||||
return
|
||||
}
|
||||
// GHOSTGRAM: When send delay is active, scheduled messages
|
||||
// don't trigger history view update, so clear input immediately.
|
||||
if SendDelayManager.shared.isEnabled {
|
||||
self.collapseInput()
|
||||
|
||||
self.ignoreUpdateHeight = true
|
||||
textInputPanelNode.text = ""
|
||||
self.requestUpdateChatInterfaceState(.immediate, overrideThreadId == nil, { state in
|
||||
@@ -4624,7 +4624,33 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
return state
|
||||
})
|
||||
self.ignoreUpdateHeight = false
|
||||
}, usedCorrelationId)
|
||||
} else {
|
||||
self.setupSendActionOnViewUpdate({ [weak self] in
|
||||
guard let self, let textInputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode else {
|
||||
return
|
||||
}
|
||||
self.collapseInput()
|
||||
|
||||
self.ignoreUpdateHeight = true
|
||||
textInputPanelNode.text = ""
|
||||
self.requestUpdateChatInterfaceState(.immediate, overrideThreadId == nil, { state in
|
||||
var state = state
|
||||
state = state.withUpdatedReplyMessageSubject(nil)
|
||||
state = state.withUpdatedSendMessageEffect(nil)
|
||||
|
||||
if state.postSuggestionState != nil {
|
||||
state = state.withUpdatedPostSuggestionState(nil)
|
||||
state = state.withUpdatedEditMessage(nil)
|
||||
}
|
||||
|
||||
state = state.withUpdatedForwardMessageIds(nil)
|
||||
state = state.withUpdatedForwardOptionsState(nil)
|
||||
state = state.withUpdatedComposeDisableUrlPreviews([])
|
||||
return state
|
||||
})
|
||||
self.ignoreUpdateHeight = false
|
||||
}, usedCorrelationId)
|
||||
}
|
||||
completion()
|
||||
|
||||
self.sendMessages(messages, silentPosting, scheduleTime, repeatPeriod, messages.count > 1, postpone)
|
||||
|
||||
@@ -2056,6 +2056,10 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
}
|
||||
}
|
||||
}
|
||||
// GHOSTGRAM: Bypass copy protection if enabled in Misc settings
|
||||
if MiscSettingsManager.shared.shouldBypassCopyProtection {
|
||||
isCopyProtectionEnabled = false
|
||||
}
|
||||
let alwaysDisplayTranscribeButton = ChatMessageItemAssociatedData.DisplayTranscribeButton(
|
||||
canBeDisplayed: suggestAudioTranscription.0 < 2,
|
||||
displayForNotConsumed: suggestAudioTranscription.1,
|
||||
@@ -2102,7 +2106,8 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
selectedMessages: selectedMessages,
|
||||
presentationData: chatPresentationData,
|
||||
historyAppearsCleared: historyAppearsCleared,
|
||||
skipViewOnceMedia: mode != .bubbles,
|
||||
// GHOSTGRAM: Keep view-once media visible if bypass is enabled
|
||||
skipViewOnceMedia: MiscSettingsManager.shared.shouldDisableViewOnceAutoDelete ? false : (mode != .bubbles),
|
||||
pendingUnpinnedAllMessages: pendingUnpinnedAllMessages,
|
||||
pendingRemovedMessages: pendingRemovedMessages,
|
||||
associatedData: associatedData,
|
||||
@@ -2110,8 +2115,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
customChannelDiscussionReadState: customChannelDiscussionReadState,
|
||||
customThreadOutgoingReadState: customThreadOutgoingReadState,
|
||||
cachedData: data.cachedData,
|
||||
adMessage: allAdMessages.fixed,
|
||||
dynamicAdMessages: allAdMessages.opportunistic
|
||||
// GHOSTGRAM: Block ads if enabled in Misc settings
|
||||
adMessage: MiscSettingsManager.shared.shouldBlockAds ? nil : allAdMessages.fixed,
|
||||
dynamicAdMessages: MiscSettingsManager.shared.shouldBlockAds ? [] : allAdMessages.opportunistic
|
||||
)
|
||||
let lastHeaderId = filteredEntries.last.flatMap { listMessageDateHeaderId(timestamp: $0.index.timestamp) } ?? 0
|
||||
let processedView = ChatHistoryView(originalView: view, filteredEntries: filteredEntries, associatedData: associatedData, lastHeaderId: lastHeaderId, id: id, locationInput: update.2, ignoreMessagesInTimestampRange: update.3, ignoreMessageIds: update.4)
|
||||
|
||||
@@ -31,7 +31,7 @@ final class ChatSearchNavigationContentNode: NavigationBarContentNode {
|
||||
self.chatLocation = chatLocation
|
||||
self.interaction = interaction
|
||||
|
||||
self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasBackground: false, hasSeparator: false), strings: strings, fieldStyle: .modern)
|
||||
self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasBackground: false, hasSeparator: false), presentationTheme: theme, strings: strings, fieldStyle: .modern)
|
||||
let placeholderText: String
|
||||
switch chatLocation {
|
||||
case .peer, .replyThread, .customChatContents:
|
||||
@@ -90,10 +90,11 @@ final class ChatSearchNavigationContentNode: NavigationBarContentNode {
|
||||
return 54.0
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
let searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - self.nominalHeight), size: CGSize(width: size.width, height: 54.0))
|
||||
self.searchBar.frame = searchBarFrame
|
||||
self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: leftInset, rightInset: rightInset, transition: transition)
|
||||
return size
|
||||
}
|
||||
|
||||
func activate() {
|
||||
@@ -106,7 +107,7 @@ final class ChatSearchNavigationContentNode: NavigationBarContentNode {
|
||||
|
||||
func update(presentationInterfaceState: ChatPresentationInterfaceState) {
|
||||
if let search = presentationInterfaceState.search {
|
||||
self.searchBar.updateThemeAndStrings(theme: SearchBarNodeTheme(theme: presentationInterfaceState.theme, hasBackground: false, hasSeparator: false), strings: presentationInterfaceState.strings)
|
||||
self.searchBar.updateThemeAndStrings(theme: SearchBarNodeTheme(theme: presentationInterfaceState.theme, hasBackground: false, hasSeparator: false), presentationTheme: presentationInterfaceState.theme, strings: presentationInterfaceState.strings)
|
||||
|
||||
switch search.domain {
|
||||
case .everything, .tag:
|
||||
|
||||
@@ -1,16 +1,4 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import ChatPresentationInterfaceState
|
||||
import AccountContext
|
||||
import class LegacyChatHeaderPanelComponent.LegacyChatTitleAccessoryPanelNode
|
||||
|
||||
class ChatTitleAccessoryPanelNode: ASDisplayNode {
|
||||
typealias LayoutResult = ChatControllerCustomNavigationPanelNode.LayoutResult
|
||||
|
||||
var interfaceInteraction: ChatPanelInterfaceInteraction?
|
||||
|
||||
func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> LayoutResult {
|
||||
preconditionFailure()
|
||||
}
|
||||
class ChatTitleAccessoryPanelNode: LegacyChatTitleAccessoryPanelNode {
|
||||
}
|
||||
|
||||
@@ -49,6 +49,8 @@ genrule(
|
||||
BUILD_ARCH="arm64"
|
||||
elif [ "$(TARGET_CPU)" == "ios_sim_arm64" ]; then
|
||||
BUILD_ARCH="sim_arm64"
|
||||
elif [ "$(TARGET_CPU)" == "ios_x86_64" ]; then
|
||||
BUILD_ARCH="sim_x86_64"
|
||||
else
|
||||
echo "Unsupported architecture $(TARGET_CPU)"
|
||||
fi
|
||||
@@ -119,4 +121,3 @@ objc_library(
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -18,6 +18,13 @@ elif [ "$ARCH" = "sim_arm64" ]; then
|
||||
custom_xcode_path="$(xcode-select -p)/"
|
||||
sed -i '' "s|/Applications/Xcode.app/Contents/Developer/|$custom_xcode_path|g" "$TARGET_CROSSFILE"
|
||||
CROSSFILE="../package/crossfiles/arm64-iPhoneSimulator-custom.meson"
|
||||
elif [ "$ARCH" = "sim_x86_64" ]; then
|
||||
TARGET_CROSSFILE="$BUILD_DIR/dav1d/package/crossfiles/x86_64-iPhoneSimulator-custom.meson"
|
||||
cp "$BUILD_DIR/dav1d/package/crossfiles/x86_64-iPhoneSimulator.meson" "$TARGET_CROSSFILE"
|
||||
custom_xcode_path="$(xcode-select -p)/"
|
||||
sed -i '' "s|/Applications/Xcode.app/Contents/Developer/|$custom_xcode_path|g" "$TARGET_CROSSFILE"
|
||||
CROSSFILE="../package/crossfiles/x86_64-iPhoneSimulator-custom.meson"
|
||||
MESON_OPTIONS="$MESON_OPTIONS -Denable_asm=false"
|
||||
else
|
||||
echo "Unsupported architecture $ARCH"
|
||||
exit 1
|
||||
@@ -33,4 +40,3 @@ ninja
|
||||
|
||||
popd
|
||||
popd
|
||||
|
||||
|
||||
@@ -37,6 +37,22 @@ elif [ "$ARCH" = "sim_arm64" ]; then
|
||||
echo "set(CMAKE_SYSTEM_PROCESSOR aarch64)" >> toolchain.cmake
|
||||
echo "set(CMAKE_C_COMPILER $(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang)" >> toolchain.cmake
|
||||
|
||||
cmake -G"Unix Makefiles" -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake -DCMAKE_OSX_SYSROOT=${IOS_SYSROOT[0]} -DPNG_SUPPORTED=FALSE -DENABLE_SHARED=FALSE -DWITH_JPEG8=1 -DBUILD=10000 -DCMAKE_POLICY_VERSION_MINIMUM=3.5 ../mozjpeg
|
||||
make
|
||||
elif [ "$ARCH" = "x86_64" ]; then
|
||||
IOS_PLATFORMDIR="$(xcode-select -p)/Platforms/iPhoneSimulator.platform"
|
||||
IOS_SYSROOT=($IOS_PLATFORMDIR/Developer/SDKs/iPhoneSimulator*.sdk)
|
||||
export CFLAGS="-Wall -arch x86_64 --target=x86_64-apple-ios13.0-simulator -miphonesimulator-version-min=13.0 -funwind-tables"
|
||||
|
||||
cd "$BUILD_DIR"
|
||||
mkdir build
|
||||
cd build
|
||||
|
||||
touch toolchain.cmake
|
||||
echo "set(CMAKE_SYSTEM_NAME Darwin)" >> toolchain.cmake
|
||||
echo "set(CMAKE_SYSTEM_PROCESSOR x86_64)" >> toolchain.cmake
|
||||
echo "set(CMAKE_C_COMPILER $(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang)" >> toolchain.cmake
|
||||
|
||||
cmake -G"Unix Makefiles" -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake -DCMAKE_OSX_SYSROOT=${IOS_SYSROOT[0]} -DPNG_SUPPORTED=FALSE -DENABLE_SHARED=FALSE -DWITH_JPEG8=1 -DBUILD=10000 -DCMAKE_POLICY_VERSION_MINIMUM=3.5 ../mozjpeg
|
||||
make
|
||||
else
|
||||
|
||||
@@ -38,8 +38,15 @@ genrule(
|
||||
BUILD_ARCH="arm64"
|
||||
elif [ "$(TARGET_CPU)" == "ios_sim_arm64" ]; then
|
||||
BUILD_ARCH="sim_arm64"
|
||||
elif [ "$(TARGET_CPU)" == "ios_x86_64" ]; then
|
||||
BUILD_ARCH="sim_x86_64"
|
||||
elif [ "$(TARGET_CPU)" == "darwin_arm64" ]; then
|
||||
BUILD_ARCH="sim_arm64"
|
||||
elif [ "$(TARGET_CPU)" == "darwin_x86_64" ]; then
|
||||
BUILD_ARCH="sim_x86_64"
|
||||
else
|
||||
echo "Unsupported architecture $(TARGET_CPU)"
|
||||
echo "Unsupported architecture $(TARGET_CPU)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BUILD_DIR="$(RULEDIR)/build_$${BUILD_ARCH}"
|
||||
|
||||
@@ -17,36 +17,91 @@ options="$options -DOPENSSL_INCLUDE_DIR=${OPENSSL_DIR}/src/include"
|
||||
options="$options -DCMAKE_BUILD_TYPE=Release"
|
||||
options="$options -DIOS_DEPLOYMENT_TARGET=13.0"
|
||||
|
||||
# Bazel genrule runs with PATH=/bin:/usr/bin, so resolve CPU count without
|
||||
# relying on /usr/sbin being in PATH.
|
||||
if [ -n "${TD_BUILD_JOBS:-}" ]; then
|
||||
BUILD_JOBS="$TD_BUILD_JOBS"
|
||||
elif [ -x /usr/sbin/sysctl ]; then
|
||||
BUILD_JOBS="$(/usr/sbin/sysctl -n hw.ncpu)"
|
||||
elif command -v getconf >/dev/null 2>&1; then
|
||||
BUILD_JOBS="$(getconf _NPROCESSORS_ONLN 2>/dev/null || true)"
|
||||
fi
|
||||
case "$BUILD_JOBS" in
|
||||
''|*[!0-9]*)
|
||||
BUILD_JOBS=8
|
||||
;;
|
||||
esac
|
||||
if [ "$BUILD_JOBS" -lt 1 ]; then
|
||||
BUILD_JOBS=1
|
||||
fi
|
||||
|
||||
MAX_BUILD_JOBS="${TD_MAX_BUILD_JOBS:-8}"
|
||||
case "$MAX_BUILD_JOBS" in
|
||||
''|*[!0-9]*)
|
||||
MAX_BUILD_JOBS=8
|
||||
;;
|
||||
esac
|
||||
if [ "$MAX_BUILD_JOBS" -lt 1 ]; then
|
||||
MAX_BUILD_JOBS=8
|
||||
fi
|
||||
if [ "$BUILD_JOBS" -gt "$MAX_BUILD_JOBS" ]; then
|
||||
BUILD_JOBS="$MAX_BUILD_JOBS"
|
||||
fi
|
||||
if [ -z "$BUILD_JOBS" ]; then
|
||||
BUILD_JOBS=8
|
||||
fi
|
||||
|
||||
cd "$BUILD_DIR"
|
||||
|
||||
# Generate source files
|
||||
mkdir native-build
|
||||
cd native-build
|
||||
cmake -DTD_GENERATE_SOURCE_FILES=ON ../td
|
||||
cmake --build . -- -j$(sysctl -n hw.ncpu)
|
||||
cmake --build . -- -j"$BUILD_JOBS"
|
||||
cd ..
|
||||
|
||||
if [ "$ARCH" = "arm64" ]; then
|
||||
IOS_PLATFORMDIR="$(xcode-select -p)/Platforms/iPhoneOS.platform"
|
||||
IOS_SYSROOT=($IOS_PLATFORMDIR/Developer/SDKs/iPhoneOS*.sdk)
|
||||
export CFLAGS="-arch arm64 --target=arm64-apple-ios13.0 -miphoneos-version-min=13.0"
|
||||
cmake_arch="arm64"
|
||||
clang_target="arm64-apple-ios13.0"
|
||||
minimum_target_flag="-miphoneos-version-min=13.0"
|
||||
cmake_processor="aarch64"
|
||||
elif [ "$ARCH" = "sim_arm64" ]; then
|
||||
IOS_PLATFORMDIR="$(xcode-select -p)/Platforms/iPhoneSimulator.platform"
|
||||
IOS_SYSROOT=($IOS_PLATFORMDIR/Developer/SDKs/iPhoneSimulator*.sdk)
|
||||
export CFLAGS="-arch arm64 --target=arm64-apple-ios13.0-simulator -miphonesimulator-version-min=13.0"
|
||||
cmake_arch="arm64"
|
||||
clang_target="arm64-apple-ios13.0-simulator"
|
||||
minimum_target_flag="-miphonesimulator-version-min=13.0"
|
||||
cmake_processor="aarch64"
|
||||
elif [ "$ARCH" = "sim_x86_64" ]; then
|
||||
IOS_PLATFORMDIR="$(xcode-select -p)/Platforms/iPhoneSimulator.platform"
|
||||
IOS_SYSROOT=($IOS_PLATFORMDIR/Developer/SDKs/iPhoneSimulator*.sdk)
|
||||
cmake_arch="x86_64"
|
||||
clang_target="x86_64-apple-ios13.0-simulator"
|
||||
minimum_target_flag="-miphonesimulator-version-min=13.0"
|
||||
cmake_processor="x86_64"
|
||||
else
|
||||
echo "Unsupported architecture $ARCH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export CFLAGS="-arch ${cmake_arch} --target=${clang_target} ${minimum_target_flag}"
|
||||
export CXXFLAGS="$CFLAGS"
|
||||
export LDFLAGS="$CFLAGS"
|
||||
|
||||
# Common build steps
|
||||
mkdir build
|
||||
cd build
|
||||
|
||||
touch toolchain.cmake
|
||||
echo "set(CMAKE_SYSTEM_NAME Darwin)" >> toolchain.cmake
|
||||
echo "set(CMAKE_SYSTEM_PROCESSOR aarch64)" >> toolchain.cmake
|
||||
echo "set(CMAKE_SYSTEM_PROCESSOR ${cmake_processor})" >> toolchain.cmake
|
||||
echo "set(CMAKE_OSX_ARCHITECTURES ${cmake_arch})" >> toolchain.cmake
|
||||
echo "set(CMAKE_C_COMPILER $(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang)" >> toolchain.cmake
|
||||
echo "set(CMAKE_CXX_COMPILER $(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang++)" >> toolchain.cmake
|
||||
echo "set(CMAKE_C_COMPILER_TARGET ${clang_target})" >> toolchain.cmake
|
||||
echo "set(CMAKE_CXX_COMPILER_TARGET ${clang_target})" >> toolchain.cmake
|
||||
|
||||
cmake -G"Unix Makefiles" -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake -DCMAKE_OSX_SYSROOT=${IOS_SYSROOT[0]} ../td $options
|
||||
make tde2e -j$(sysctl -n hw.ncpu)
|
||||
make tde2e -j"$BUILD_JOBS"
|
||||
|
||||