Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2023-03-31 23:45:50 +04:00
commit 6f4f3817a9
54 changed files with 857 additions and 284 deletions

View File

@ -35,6 +35,3 @@ build --spawn_strategy=standalone
build --strategy=SwiftCompile=standalone
build --define RULES_SWIFT_BUILD_DUMMY_WORKER=1
#build --linkopt=-fuse-ld=/Users/ali/Downloads/ld64.lld
#build --linkopt=-fuse-ld=/Users/ali/build/zld/build/Build/Products/Release/zld
#build --linkopt=-Wl,-zld_original_ld_path,__BAZEL_XCODE_DEVELOPER_DIR__/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld

1
.gitignore vendored
View File

@ -59,6 +59,7 @@ bazel-telegram-ios
bazel-telegram-ios/*
bazel-testlogs
bazel-testlogs/*
xcodeproj.bazelrc
*/*.swp
*.swp
build-input/*

3
.gitmodules vendored
View File

@ -29,3 +29,6 @@ url=../tgcalls.git
[submodule "third-party/libx264/x264"]
path = third-party/libx264/x264
url = https://github.com/mirror/x264.git
[submodule "build-system/bazel-rules/rules_xcodeproj"]
path = build-system/bazel-rules/rules_xcodeproj
url = https://github.com/MobileNativeFoundation/rules_xcodeproj.git

View File

@ -25,12 +25,19 @@ load("@build_bazel_rules_swift//swift:swift.bzl",
"swift_library",
)
load(
"@rules_xcodeproj//xcodeproj:defs.bzl",
"top_level_target",
"xcodeproj",
)
load("//build-system/bazel-utils:plist_fragment.bzl",
"plist_fragment",
)
load(
"@build_configuration//:variables.bzl",
"telegram_bazel_path",
"telegram_bundle_id",
"telegram_aps_environment",
"telegram_team_id",
@ -1010,29 +1017,6 @@ plist_fragment(
)
)
ios_framework(
name = "AsyncDisplayKitFramework",
bundle_id = "{telegram_bundle_id}.AsyncDisplayKit".format(
telegram_bundle_id = telegram_bundle_id,
),
families = [
"iphone",
"ipad",
],
infoplists = [
":AsyncDisplayKitInfoPlist",
":BuildNumberInfoPlist",
":VersionInfoPlist",
":RequiredDeviceCapabilitiesPlist",
],
minimum_os_version = minimum_os_version,
extension_safe = True,
ipa_post_processor = strip_framework,
deps = [
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
],
)
plist_fragment(
name = "DisplayInfoPlist",
extension = "plist",
@ -1094,33 +1078,6 @@ sh_binary(
srcs = [":GenerateAddAlternateIcons"],
)
ios_framework(
name = "DisplayFramework",
bundle_id = "{telegram_bundle_id}.Display".format(
telegram_bundle_id = telegram_bundle_id,
),
families = [
"iphone",
"ipad",
],
infoplists = [
":DisplayInfoPlist",
":BuildNumberInfoPlist",
":VersionInfoPlist",
":RequiredDeviceCapabilitiesPlist",
],
frameworks = [
":SwiftSignalKitFramework",
":AsyncDisplayKitFramework",
],
minimum_os_version = minimum_os_version,
extension_safe = True,
ipa_post_processor = strip_framework,
deps = [
"//submodules/Display:Display",
],
)
plist_fragment(
name = "TelegramUIInfoPlist",
extension = "plist",
@ -1159,8 +1116,6 @@ ios_framework(
":SwiftSignalKitFramework",
":PostboxFramework",
":TelegramCoreFramework",
":AsyncDisplayKitFramework",
":DisplayFramework",
],
minimum_os_version = minimum_os_version,
extension_safe = True,
@ -1997,8 +1952,6 @@ ios_application(
":SwiftSignalKitFramework",
":PostboxFramework",
":TelegramCoreFramework",
":AsyncDisplayKitFramework",
":DisplayFramework",
":TelegramUIFramework",
],
strings = [
@ -2023,6 +1976,18 @@ ios_application(
":Main",
":Lib",
],
visibility = ["//visibility:public"],
)
xcodeproj(
name = "Telegram_xcodeproj",
build_mode = "bazel",
bazel_path = telegram_bazel_path,
project_name = "Telegram",
tags = ["manual"],
top_level_targets = [
":Telegram",
],
)
# Temporary targets used to simplify webrtc build tests

View File

@ -9122,3 +9122,7 @@ Sorry for the inconvenience.";
"Notification.ChangedWallpaper" = "%1$@ set a new background for this chat";
"Notification.YouChangedWallpaper" = "You set a new background for this chat";
"Notification.Wallpaper.View" = "View Background";
"Channel.AdminLog.JoinedViaFolderInviteLink" = "%1$@ joined via invite link %2$@ (community)";
"Conversation.OpenChatFolder" = "Open Shared Folder";

View File

@ -1,15 +1,15 @@
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file")
http_archive(
'''http_archive(
name = "com_google_protobuf",
urls = ["https://github.com/protocolbuffers/protobuf/archive/v3.14.0.zip"],
sha256 = "bf0e5070b4b99240183b29df78155eee335885e53a8af8683964579c214ad301",
strip_prefix = "protobuf-3.14.0",
urls = ["https://github.com/protocolbuffers/protobuf/releases/download/v22.2/protobuf-22.2.zip"],
sha256 = "bf1f92aebd619651220711e97b3d560cdc2484718cd56d95161bfb2fadb8628e",
strip_prefix = "protobuf-22.2",
type = "zip",
)
load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps")
protobuf_deps()
protobuf_deps()'''
local_repository(
name = "build_bazel_rules_apple",
@ -26,6 +26,11 @@ local_repository(
path = "build-system/bazel-rules/apple_support",
)
local_repository(
name = "rules_xcodeproj",
path = "build-system/bazel-rules/rules_xcodeproj",
)
load(
"@build_bazel_rules_apple//apple:repositories.bzl",
"apple_rules_dependencies",
@ -47,6 +52,13 @@ load(
apple_support_dependencies()
load(
"@rules_xcodeproj//xcodeproj:repositories.bzl",
"xcodeproj_rules_dependencies",
)
xcodeproj_rules_dependencies()
load("@bazel_skylib//:workspace.bzl", "bazel_skylib_workspace")
bazel_skylib_workspace()

View File

@ -35,8 +35,9 @@ class BuildConfiguration:
self.enable_siri = enable_siri
self.enable_icloud = enable_icloud
def write_to_variables_file(self, aps_environment, path):
def write_to_variables_file(self, bazel_path, aps_environment, path):
string = ''
string += 'telegram_bazel_path = "{}"\n'.format(bazel_path)
string += 'telegram_bundle_id = "{}"\n'.format(self.bundle_id)
string += 'telegram_api_id = "{}"\n'.format(self.api_id)
string += 'telegram_api_hash = "{}"\n'.format(self.api_hash)

View File

@ -30,6 +30,7 @@ class BazelCommandLine:
override_bazel_version=override_bazel_version,
override_xcode_version=override_xcode_version
)
self.bazel = bazel
self.bazel_user_root = bazel_user_root
self.remote_cache = None
self.cache_dir = None
@ -497,7 +498,8 @@ def resolve_configuration(base_path, bazel_command_line: BazelCommandLine, argum
print('Could not find a valid aps-environment entitlement in the provided provisioning profiles')
sys.exit(1)
build_configuration.write_to_variables_file(aps_environment=codesigning_data.aps_environment, path=configuration_repository_path + '/variables.bzl')
if bazel_command_line is not None:
build_configuration.write_to_variables_file(bazel_path=bazel_command_line.bazel, aps_environment=codesigning_data.aps_environment, path=configuration_repository_path + '/variables.bzl')
provisioning_profile_files = []
for file_name in os.listdir(provisioning_path):

View File

@ -9,8 +9,37 @@ def remove_directory(path):
if os.path.isdir(path):
shutil.rmtree(path)
def generate_xcodeproj(build_environment: BuildEnvironment, disable_extensions, disable_provisioning_profiles, generate_dsym, configuration_path, bazel_app_arguments, target_name):
bazel_generate_arguments = [build_environment.bazel_path]
bazel_generate_arguments += ['run', '//Telegram:Telegram_xcodeproj']
bazel_generate_arguments += ['--override_repository=build_configuration={}'.format(configuration_path)]
#if disable_extensions:
# bazel_generate_arguments += ['--//{}:disableExtensions'.format(app_target)]
#if disable_provisioning_profiles:
# bazel_generate_arguments += ['--//{}:disableProvisioningProfiles'.format(app_target)]
#if generate_dsym:
# bazel_generate_arguments += ['--apple_generate_dsym']
#bazel_generate_arguments += ['--//{}:disableStripping'.format('Telegram')]
def generate(build_environment: BuildEnvironment, disable_extensions, disable_provisioning_profiles, generate_dsym, configuration_path, bazel_app_arguments, target_name):
project_bazel_arguments = []
for argument in bazel_app_arguments:
project_bazel_arguments.append(argument)
project_bazel_arguments += ['--override_repository=build_configuration={}'.format(configuration_path)]
xcodeproj_bazelrc = os.path.join(build_environment.base_path, 'xcodeproj.bazelrc')
if os.path.isfile(xcodeproj_bazelrc):
os.unlink(xcodeproj_bazelrc)
with open(xcodeproj_bazelrc, 'w') as file:
for argument in project_bazel_arguments:
file.write('build ' + argument + '\n')
call_executable(bazel_generate_arguments)
xcodeproj_path = 'Telegram/Telegram.xcodeproj'
call_executable(['open', xcodeproj_path])
def generate_tulsi(build_environment: BuildEnvironment, disable_extensions, disable_provisioning_profiles, generate_dsym, configuration_path, bazel_app_arguments, target_name):
project_path = os.path.join(build_environment.base_path, 'build-input/gen/project')
if '/' in target_name:
@ -141,3 +170,9 @@ def generate(build_environment: BuildEnvironment, disable_extensions, disable_pr
xcodeproj_path = '{project}/{target}.xcodeproj'.format(project=project_path, target=app_target_clean)
call_executable(['open', xcodeproj_path])
def generate(build_environment: BuildEnvironment, disable_extensions, disable_provisioning_profiles, generate_dsym, configuration_path, bazel_app_arguments, target_name):
generate_xcodeproj(build_environment, disable_extensions, disable_provisioning_profiles, generate_dsym, configuration_path, bazel_app_arguments, target_name)
#generate_tulsi(build_environment, disable_extensions, disable_provisioning_profiles, generate_dsym, configuration_path, bazel_app_arguments, target_name)

@ -1 +1 @@
Subproject commit 2659bae1f561e34b89fcc230df26aaf6dada2646
Subproject commit 7cecc126925fef811c274b10b99329179574f9cb

@ -1 +1 @@
Subproject commit 782fa6bb135b5a9a31ab7884e9ebfaac29ef71d9
Subproject commit 488633f6fa04cbc9310079e368955b9863754e64

@ -1 +1 @@
Subproject commit 371ab0507ab2318f0936adde3bcebbb1ccacf3a8
Subproject commit e01b8f76e7666ac254f1780b00de33c1296da8ff

@ -0,0 +1 @@
Subproject commit dc226d129aca2237982b98a95c80ed1ccc74f0c5

View File

@ -22,15 +22,11 @@ def _plist_fragment(ctx):
fail("Expected value for --define={} was not found".format(key))
resolved_values[key] = value
plist_string = """
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
""" + template.format(**resolved_values) + """
</dict>
</plist>
"""
plist_string = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>""" + template.format(**resolved_values) + """</dict>
</plist>"""
ctx.actions.write(
output = output,

@ -1 +1 @@
Subproject commit 2b8fbf5a95d43dd43ba20b5a690c2bf8e1146063
Subproject commit a0bf60e1645869c6452c9f3b128362d433764f19

View File

@ -20,4 +20,6 @@ public protocol ChatListController: ViewController {
func maybeAskForPeerChatRemoval(peer: EngineRenderedPeer, joined: Bool, deleteGloballyIfPossible: Bool, completion: @escaping (Bool) -> Void, removed: @escaping () -> Void)
func playSignUpCompletedAnimation()
func navigateToFolder(folderId: Int32, completion: @escaping () -> Void)
}

View File

@ -1546,7 +1546,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
for filter in filters {
if filter.id == filterId, case let .filter(_, title, _, data) = filter {
if !data.includePeers.peers.isEmpty {
if !data.includePeers.peers.isEmpty && data.categories.isEmpty && !data.excludeRead && !data.excludeMuted && !data.excludeArchived && data.excludePeers.isEmpty {
items.append(.action(ContextMenuActionItem(text: "Share", textColor: .primary, badge: ContextMenuActionBadge(value: "NEW", color: .accent, style: .label), icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor)
}, action: { c, f in
@ -2709,6 +2709,37 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
})
}
public func navigateToFolder(folderId: Int32, completion: @escaping () -> Void) {
let _ = (self.chatListDisplayNode.mainContainerNode.availableFiltersSignal
|> filter { filters in
return filters.contains(where: { item in
if case let .filter(filter) = item, filter.id == folderId {
return true
} else {
return false
}
})
}
|> take(1)
|> map { _ -> Bool in
return true
}
|> timeout(1.0, queue: .mainQueue(), alternate: .single(false))
|> deliverOnMainQueue).start(next: { [weak self] _ in
guard let self else {
return
}
if self.chatListDisplayNode.mainContainerNode.currentItemNode.chatListFilter?.id != folderId {
self.chatListDisplayNode.mainContainerNode.switchToFilter(id: .filter(folderId), completion: {
completion()
})
} else {
completion()
}
})
}
private func askForFilterRemoval(id: Int32) {
let apply: () -> Void = { [weak self] in
guard let strongSelf = self else {
@ -2753,17 +2784,28 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if case let .filter(_, title, _, data) = filter, data.isShared {
let _ = (combineLatest(
self.context.engine.data.get(
EngineDataList(data.includePeers.peers.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)))
EngineDataList(data.includePeers.peers.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))),
EngineDataMap(data.includePeers.peers.map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init(id:)))
),
self.context.engine.peers.getExportedChatFolderLinks(id: id)
self.context.engine.peers.getExportedChatFolderLinks(id: id),
self.context.engine.peers.requestLeaveChatFolderSuggestions(folderId: id)
)
|> deliverOnMainQueue).start(next: { [weak self] peers, links in
|> deliverOnMainQueue).start(next: { [weak self] peerData, links, defaultSelectedPeerIds in
guard let self else {
return
}
let presentationData = self.presentationData
let peers = peerData.0
var memberCounts: [EnginePeer.Id: Int] = [:]
for (id, count) in peerData.1 {
if let count {
memberCounts[id] = count
}
}
var hasLinks = false
if let links, !links.isEmpty {
hasLinks = true
@ -2776,7 +2818,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let previewScreen = ChatFolderLinkPreviewScreen(
context: self.context,
subject: .remove(folderId: id),
subject: .remove(folderId: id, defaultSelectedPeerIds: defaultSelectedPeerIds),
contents: ChatFolderLinkContents(
localFilterId: id,
title: title,
@ -2787,7 +2829,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return false
}
},
alreadyMemberPeerIds: Set()
alreadyMemberPeerIds: Set(),
memberCounts: memberCounts
),
completion: { [weak self] in
guard let self else {

View File

@ -513,7 +513,16 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
private var itemNodes: [ChatListFilterTabEntryId: ChatListContainerItemNode] = [:]
private var pendingItemNode: (ChatListFilterTabEntryId, ChatListContainerItemNode, Disposable)?
private(set) var availableFilters: [ChatListContainerNodeFilter] = [.all]
private(set) var availableFilters: [ChatListContainerNodeFilter] = [.all] {
didSet {
self.availableFiltersPromise.set(self.availableFilters)
}
}
private let availableFiltersPromise = ValuePromise<[ChatListContainerNodeFilter]>([.all], ignoreRepeated: true)
var availableFiltersSignal: Signal<[ChatListContainerNodeFilter], NoError> {
return self.availableFiltersPromise.get()
}
private var filtersLimit: Int32? = nil
private var selectedId: ChatListFilterTabEntryId

View File

@ -31,10 +31,12 @@ private final class ChatListFilterPresetControllerArguments {
let setItemIdWithRevealedOptions: (ChatListFilterRevealedItemId?, ChatListFilterRevealedItemId?) -> Void
let deleteIncludeCategory: (ChatListFilterIncludeCategory) -> Void
let deleteExcludeCategory: (ChatListFilterExcludeCategory) -> Void
let clearFocus: () -> Void
let focusOnName: () -> Void
let expandSection: (FilterSection) -> Void
let createLink: () -> Void
let openLink: (ExportedChatFolderLink) -> Void
let removeLink: (ExportedChatFolderLink) -> Void
init(
context: AccountContext,
@ -46,10 +48,12 @@ private final class ChatListFilterPresetControllerArguments {
setItemIdWithRevealedOptions: @escaping (ChatListFilterRevealedItemId?, ChatListFilterRevealedItemId?) -> Void,
deleteIncludeCategory: @escaping (ChatListFilterIncludeCategory) -> Void,
deleteExcludeCategory: @escaping (ChatListFilterExcludeCategory) -> Void,
clearFocus: @escaping () -> Void,
focusOnName: @escaping () -> Void,
expandSection: @escaping (FilterSection) -> Void,
createLink: @escaping () -> Void,
openLink: @escaping (ExportedChatFolderLink) -> Void
openLink: @escaping (ExportedChatFolderLink) -> Void,
removeLink: @escaping (ExportedChatFolderLink) -> Void
) {
self.context = context
self.updateState = updateState
@ -60,10 +64,12 @@ private final class ChatListFilterPresetControllerArguments {
self.setItemIdWithRevealedOptions = setItemIdWithRevealedOptions
self.deleteIncludeCategory = deleteIncludeCategory
self.deleteExcludeCategory = deleteExcludeCategory
self.clearFocus = clearFocus
self.focusOnName = focusOnName
self.expandSection = expandSection
self.createLink = createLink
self.openLink = openLink
self.removeLink = removeLink
}
}
@ -316,10 +322,10 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
case excludePeerInfo(String)
case includeExpand(String)
case excludeExpand(String)
case inviteLinkHeader
case inviteLinkHeader(hasLinks: Bool)
case inviteLinkCreate(hasLinks: Bool)
case inviteLink(Int, ExportedChatFolderLink)
case inviteLinkInfo
case inviteLinkInfo(text: String)
var section: ItemListSectionId {
switch self {
@ -434,14 +440,16 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
case let .nameHeader(title):
return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section)
case let .name(placeholder, value):
return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: value, placeholder: placeholder, type: .regular(capitalization: true, autocorrection: false), clearType: .always, maxLength: 12, sectionId: self.section, textUpdated: { value in
return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(), text: value, placeholder: placeholder, type: .regular(capitalization: true, autocorrection: false), returnKeyType: .done, clearType: .always, maxLength: 12, sectionId: self.section, textUpdated: { value in
arguments.updateState { current in
var state = current
state.name = value
state.changedName = true
return state
}
}, action: {}, cleared: {
}, action: {
arguments.clearFocus()
}, cleared: {
arguments.focusOnName()
})
case .includePeersHeader(let text), .excludePeersHeader(let text):
@ -516,23 +524,23 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.downArrowImage(presentationData.theme), title: text, sectionId: self.section, editing: false, action: {
arguments.expandSection(.exclude)
})
case .inviteLinkHeader:
case let .inviteLinkHeader(hasLinks):
//TODO:localize
return ItemListSectionHeaderItem(presentationData: presentationData, text: "INVITE LINK", badge: "NEW", sectionId: self.section)
return ItemListSectionHeaderItem(presentationData: presentationData, text: "SHARE FOLDER", badge: hasLinks ? nil : "NEW", sectionId: self.section)
case let .inviteLinkCreate(hasLinks):
//TODO:localize
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.linkIcon(presentationData.theme), title: hasLinks ? "Create a New Link" : "Share Folder", sectionId: self.section, editing: false, action: {
let _ = hasLinks
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.linkIcon(presentationData.theme), title: "Create an Invite Link", sectionId: self.section, editing: false, action: {
arguments.createLink()
})
case let .inviteLink(_, link):
return ItemListFolderInviteLinkListItem(presentationData: presentationData, invite: link, share: false, sectionId: self.section, style: .blocks) { invite in
return ItemListFolderInviteLinkListItem(presentationData: presentationData, invite: link, share: false, sectionId: self.section, style: .blocks, tapAction: { invite in
arguments.openLink(invite)
} contextAction: { invite, node, gesture in
//arguments.linkContextAction(invite, canEdit, node, gesture)
}
case .inviteLinkInfo:
//TODO:localize
return ItemListTextItem(presentationData: presentationData, text: .markdown("Share access to some of this folder's groups and channels with others."), sectionId: self.section)
}, removeAction: { invite in
arguments.removeLink(invite)
}, contextAction: nil)
case let .inviteLinkInfo(text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
}
}
}
@ -654,26 +662,25 @@ private func chatListFilterPresetControllerEntries(presentationData: Presentatio
entries.append(.excludePeerInfo(presentationData.strings.ChatListFolder_ExcludeSectionInfo))
}
if !isNewFilter {
entries.append(.inviteLinkHeader)
var hasLinks = false
if let inviteLinks, !inviteLinks.isEmpty {
hasLinks = true
}
entries.append(.inviteLinkCreate(hasLinks: hasLinks))
if let inviteLinks {
var index = 0
for link in inviteLinks {
entries.append(.inviteLink(index, link))
index += 1
}
}
entries.append(.inviteLinkInfo)
var hasLinks = false
if let inviteLinks, !inviteLinks.isEmpty {
hasLinks = true
}
entries.append(.inviteLinkHeader(hasLinks: hasLinks))
entries.append(.inviteLinkCreate(hasLinks: hasLinks))
if let inviteLinks {
var index = 0
for link in inviteLinks {
entries.append(.inviteLink(index, link))
index += 1
}
}
//TODO:localize
entries.append(.inviteLinkInfo(text: hasLinks ? "Create more links to set up different access levels for different people." : "Share access to some of this folder's groups and channels with others."))
return entries
}
@ -1072,6 +1079,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
var pushControllerImpl: ((ViewController) -> Void)?
var dismissImpl: (() -> Void)?
var focusOnNameImpl: (() -> Void)?
var clearFocusImpl: (() -> Void)?
var applyImpl: ((@escaping () -> Void) -> Void)?
let sharedLinks = Promise<[ExportedChatFolderLink]?>(nil)
@ -1270,6 +1278,9 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
return state
}
},
clearFocus: {
clearFocusImpl?()
},
focusOnName: {
focusOnNameImpl?()
},
@ -1281,44 +1292,62 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
}
},
createLink: {
applyImpl?({
let state = stateValue.with({ $0 })
if let currentPreset, let data = currentPreset.data {
//TODO:localize
var unavailableText: String?
if !data.categories.isEmpty || data.excludeArchived || data.excludeRead || data.excludeMuted || !data.excludePeers.isEmpty {
unavailableText = "You can't share a link to this folder."
}
if let unavailableText {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: unavailableText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
return
}
if currentPreset == nil {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let text = "Please finish creating this folder to share it."
presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
} else {
applyImpl?({
let state = stateValue.with({ $0 })
openCreateChatListFolderLink(context: context, folderId: currentPreset.id, checkIfExists: false, title: currentPreset.title, peerIds: state.additionallyIncludePeers, pushController: { c in
pushControllerImpl?(c)
}, presentController: { c in
presentControllerImpl?(c, nil)
}, linkUpdated: { updatedLink in
let _ = (sharedLinks.get() |> take(1) |> deliverOnMainQueue).start(next: { links in
guard var links else {
return
}
if let currentPreset, let data = currentPreset.data {
//TODO:localize
var unavailableText: String?
if !data.categories.isEmpty {
unavailableText = "You cant share folders with include chat types."
} else if data.excludeArchived || data.excludeRead || data.excludeMuted {
unavailableText = "You can only share folders without chat types and excluded chats."
} else if !data.excludePeers.isEmpty {
unavailableText = "You cant share folders with excluded chats"
}
if let unavailableText {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: unavailableText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
if let updatedLink {
if let index = links.firstIndex(where: { $0.link == updatedLink.link }) {
links[index] = updatedLink
} else {
links.insert(updatedLink, at: 0)
return
}
var previousLink: ExportedChatFolderLink?
openCreateChatListFolderLink(context: context, folderId: currentPreset.id, checkIfExists: false, title: currentPreset.title, peerIds: state.additionallyIncludePeers, pushController: { c in
pushControllerImpl?(c)
}, presentController: { c in
presentControllerImpl?(c, nil)
}, linkUpdated: { updatedLink in
let previousLinkValue = previousLink
previousLink = updatedLink
let _ = (sharedLinks.get() |> take(1) |> deliverOnMainQueue).start(next: { links in
guard var links else {
return
}
if let updatedLink {
if let index = links.firstIndex(where: { $0.link == updatedLink.link }) {
links[index] = updatedLink
} else {
links.insert(updatedLink, at: 0)
}
} else if let previousLinkValue {
if let index = links.firstIndex(where: { $0.link == previousLinkValue.link }) {
links.remove(at: index)
}
}
sharedLinks.set(.single(links))
}
})
})
})
}
})
}
})
}
}, openLink: { link in
if let currentPreset, let _ = currentPreset.data {
applyImpl?({
@ -1331,13 +1360,13 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
}
if let updatedLink {
if let index = links.firstIndex(where: { $0 == link }) {
if let index = links.firstIndex(where: { $0.link == link.link }) {
links.remove(at: index)
}
links.insert(updatedLink, at: 0)
sharedLinks.set(.single(links))
} else {
if let index = links.firstIndex(where: { $0 == link }) {
if let index = links.firstIndex(where: { $0.link == link.link }) {
links.remove(at: index)
sharedLinks.set(.single(links))
}
@ -1347,6 +1376,22 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
}))
})
}
},
removeLink: { link in
if let currentPreset {
let _ = (sharedLinks.get() |> take(1) |> deliverOnMainQueue).start(next: { links in
guard var links else {
return
}
if let index = links.firstIndex(where: { $0.link == link.link }) {
links.remove(at: index)
}
sharedLinks.set(.single(links))
actionsDisposable.add(context.engine.peers.deleteChatFolderLink(filterId: currentPreset.id, link: link).start())
})
}
}
)
@ -1462,6 +1507,12 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
}
}
}
clearFocusImpl = { [weak controller] in
guard let controller = controller else {
return
}
controller.view.endEditing(true)
}
controller.attemptNavigation = { _ in
return attemptNavigationImpl?() ?? true
}
@ -1493,7 +1544,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset: Chat
return false
}
} else {
if state.isComplete {
if currentPreset != nil, state.isComplete {
displaySaveAlert()
return false
}
@ -1575,15 +1626,18 @@ func openCreateChatListFolderLink(context: AccountContext, folderId: Int32, chec
switch error {
case .generic:
text = "An error occurred"
case let .limitExceeded(limit, premiumLimit):
if limit < premiumLimit {
let limitController = context.sharedContext.makePremiumLimitController(context: context, subject: .linksPerSharedFolder, count: limit, action: {
})
pushController(limitController)
return
}
text = "You can't create more links."
case let .sharedFolderLimitExceeded(limit, _):
let limitController = context.sharedContext.makePremiumLimitController(context: context, subject: .membershipInSharedFolders, count: limit, action: {
})
pushController(limitController)
return
case let .limitExceeded(limit, _):
let limitController = context.sharedContext.makePremiumLimitController(context: context, subject: .linksPerSharedFolder, count: limit, action: {
})
pushController(limitController)
return
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentController(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]))

View File

@ -393,13 +393,23 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
if case let .filter(_, title, _, data) = filter, data.isShared {
let _ = (combineLatest(
context.engine.data.get(
EngineDataList(data.includePeers.peers.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)))
EngineDataList(data.includePeers.peers.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:))),
EngineDataMap(data.includePeers.peers.map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init(id:)))
),
context.engine.peers.getExportedChatFolderLinks(id: id)
context.engine.peers.getExportedChatFolderLinks(id: id),
context.engine.peers.requestLeaveChatFolderSuggestions(folderId: id)
)
|> deliverOnMainQueue).start(next: { peers, links in
|> deliverOnMainQueue).start(next: { peerData, links, defaultSelectedPeerIds in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let peers = peerData.0
var memberCounts: [EnginePeer.Id: Int] = [:]
for (id, count) in peerData.1 {
if let count {
memberCounts[id] = count
}
}
var hasLinks = false
if let links, !links.isEmpty {
hasLinks = true
@ -408,7 +418,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
let confirmDeleteFolder: () -> Void = {
let previewScreen = ChatFolderLinkPreviewScreen(
context: context,
subject: .remove(folderId: id),
subject: .remove(folderId: id, defaultSelectedPeerIds: defaultSelectedPeerIds),
contents: ChatFolderLinkContents(
localFilterId: id,
title: title,
@ -419,7 +429,8 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
return false
}
},
alreadyMemberPeerIds: Set()
alreadyMemberPeerIds: Set(),
memberCounts: memberCounts
)
)
pushControllerImpl?(previewScreen)

View File

@ -215,7 +215,7 @@ private final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemN
} else {
updateArrowImage = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme)
}
updatedSharedIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Links/Share"), color: item.presentationData.theme.list.disclosureArrowColor)
updatedSharedIconImage = generateTintedImage(image: UIImage(bundleImageName: "Chat List/SharedFolderListIcon"), color: item.presentationData.theme.list.disclosureArrowColor)
}
let peerRevealOptions: [ItemListRevealOption]

View File

@ -69,7 +69,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
case mainLink(link: ExportedChatFolderLink?, isGenerating: Bool)
case peersHeader(String)
case peer(index: Int, peer: EnginePeer, isSelected: Bool, isEnabled: Bool)
case peer(index: Int, peer: EnginePeer, isSelected: Bool, disabledReasonText: String?)
case peersInfo(String)
var section: ItemListSectionId {
@ -149,8 +149,8 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
} else {
return false
}
case let .peer(index, peer, isSelected, isEnabled):
if case .peer(index, peer, isSelected, isEnabled) = rhs {
case let .peer(index, peer, isSelected, disabledReasonText):
if case .peer(index, peer, isSelected, disabledReasonText) = rhs {
return true
} else {
return false
@ -195,7 +195,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .peersInfo(text):
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
case let .peer(_, peer, isSelected, isEnabled):
case let .peer(_, peer, isSelected, disabledReasonText):
//TODO:localize
return ItemListPeerItem(
presentationData: presentationData,
@ -204,16 +204,16 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
context: arguments.context,
peer: peer,
presence: nil,
text: .text(isEnabled ? "you can invite others here" : "you can't invite others here", .secondary),
text: .text(disabledReasonText ?? "you can invite others here", .secondary),
label: .none,
editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false),
switchValue: ItemListPeerItemSwitch(value: isSelected, style: .leftCheck, isEnabled: isEnabled),
switchValue: ItemListPeerItemSwitch(value: isSelected, style: .leftCheck, isEnabled: disabledReasonText == nil),
enabled: true,
selectable: true,
highlightable: false,
sectionId: self.section,
action: {
arguments.peerAction(peer, isEnabled)
arguments.peerAction(peer, disabledReasonText == nil)
},
setPeerIdWithRevealedOptions: { _, _ in
},
@ -274,8 +274,19 @@ private func folderInviteLinkListControllerEntries(
}
for peer in sortedPeers {
let isEnabled = canShareLinkToPeer(peer: peer)
entries.append(.peer(index: entries.count, peer: peer, isSelected: state.selectedPeerIds.contains(peer.id), isEnabled: isEnabled))
var disabledReasonText: String?
if !canShareLinkToPeer(peer: peer) {
if case let .user(user) = peer {
if user.botInfo != nil {
disabledReasonText = "you can't share chats with bots"
} else {
disabledReasonText = "you can't share private chats"
}
} else {
disabledReasonText = "you can't invite other here"
}
}
entries.append(.peer(index: entries.count, peer: peer, isSelected: state.selectedPeerIds.contains(peer.id), disabledReasonText: disabledReasonText))
}
if let infoString {
@ -391,7 +402,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
let state = stateValue.with({ $0 })
let promptController = promptController(sharedContext: context.sharedContext, updatedPresentationData: updatedPresentationData, text: "Link title", value: state.title ?? "", apply: { value in
let promptController = promptController(sharedContext: context.sharedContext, updatedPresentationData: updatedPresentationData, text: "Name This Link", titleFont: .bold, value: state.title ?? "", apply: { value in
if let value {
updateState { state in
var state = state
@ -458,7 +469,10 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
state.selectedPeerIds.remove(peer.id)
} else {
state.selectedPeerIds.insert(peer.id)
added = true
if let currentInvitation, !currentInvitation.peerIds.contains(peer.id) {
added = true
}
}
return state
@ -469,7 +483,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
dismissTooltipsImpl?()
//TODO:localize
displayTooltipImpl?(.info(title: nil, text: "People who already used the invite link will be able to join newly added chats."))
displayTooltipImpl?(.info(title: nil, text: "People who already used the invite link will be able to join newly added chats.", timeout: 8))
}
} else {
//TODO:localize
@ -549,7 +563,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
dismissTooltipsImpl?()
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: "An error occurred."), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: "An error occurred.", timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
}, completed: {
linkUpdated(ExportedChatFolderLink(title: state.title ?? "", link: currentLink.link, peerIds: Array(state.selectedPeerIds), isRevoked: false))
dismissImpl?()
@ -587,6 +601,8 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
}
})
let previousState = Atomic<FolderInviteLinkListControllerState?>(value: nil)
let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData
let signal = combineLatest(queue: .mainQueue(),
presentationData,
@ -595,7 +611,13 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
)
|> map { presentationData, state, allPeers -> (ItemListControllerState, (ItemListNodeState, Any)) in
let crossfade = false
let animateChanges = false
var animateChanges = false
let previousStateValue = previousState.swap(state)
if let previousStateValue, previousStateValue.selectedPeerIds != state.selectedPeerIds {
animateChanges = true
}
//TODO:localize
let title: ItemListControllerTitle
@ -610,7 +632,7 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
if state.isSaving {
doneButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {})
} else {
doneButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: {
doneButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Save), style: .bold, enabled: !state.selectedPeerIds.isEmpty, action: {
applyChangesImpl?()
})
}

View File

@ -66,7 +66,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
case requestHeader(PresentationTheme, String, String, Bool)
case request(Int32, PresentationTheme, PresentationDateTimeFormat, EnginePeer, Int32, Bool)
case importerHeader(PresentationTheme, String, String, Bool)
case importer(Int32, PresentationTheme, PresentationDateTimeFormat, EnginePeer, Int32, Bool)
case importer(Int32, PresentationTheme, PresentationDateTimeFormat, EnginePeer, Int32, Bool, Bool)
var stableId: InviteLinkViewEntryId {
switch self {
@ -82,7 +82,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
return .request(peer.id)
case .importerHeader:
return .importerHeader
case let .importer(_, _, _, peer, _, _):
case let .importer(_, _, _, peer, _, _, _):
return .importer(peer.id)
}
}
@ -125,8 +125,8 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
} else {
return false
}
case let .importer(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsDate, lhsLoading):
if case let .importer(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsPeer, rhsDate, rhsLoading) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsPeer == rhsPeer, lhsDate == rhsDate, lhsLoading == rhsLoading {
case let .importer(lhsIndex, lhsTheme, lhsDateTimeFormat, lhsPeer, lhsDate, lhsJoinedViaFolderLink, lhsLoading):
if case let .importer(rhsIndex, rhsTheme, rhsDateTimeFormat, rhsPeer, rhsDate, rhsJoinedViaFolderLink, rhsLoading) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsPeer == rhsPeer, lhsDate == rhsDate, lhsJoinedViaFolderLink == rhsJoinedViaFolderLink, lhsLoading == rhsLoading {
return true
} else {
return false
@ -180,11 +180,11 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
case .importer:
return true
}
case let .importer(lhsIndex, _, _, _, _, _):
case let .importer(lhsIndex, _, _, _, _, _, _):
switch rhs {
case .link, .creatorHeader, .creator, .importerHeader, .request, .requestHeader:
return false
case let .importer(rhsIndex, _, _, _, _, _):
case let .importer(rhsIndex, _, _, _, _, _, _):
return lhsIndex < rhsIndex
}
}
@ -224,7 +224,18 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
additionalText = .none
}
return SectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title, additionalText: additionalText)
case let .importer(_, _, dateTimeFormat, peer, date, loading), let .request(_, _, dateTimeFormat, peer, date, loading):
case let .importer(_, _, dateTimeFormat, peer, date, joinedViaFolderLink, loading):
let dateString: String
if joinedViaFolderLink {
//TODO:localize
dateString = "joined via a folder invite link"
} else {
dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat)
}
return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .generic, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: {
interaction.openPeer(peer.id)
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, hasTopStripe: false, noInsets: true, tag: nil, shimmering: loading ? ItemListPeerItemShimmering(alternationIndex: 0) : nil)
case let .request(_, _, dateTimeFormat, peer, date, loading):
let dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat)
return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: interaction.context, peer: peer, height: .generic, nameStyle: .distinctBold, presence: nil, text: .text(dateString, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), revealOptions: nil, switchValue: nil, enabled: true, selectable: peer.id != account.peerId, sectionId: 0, action: {
interaction.openPeer(peer.id)
@ -753,14 +764,14 @@ public final class InviteLinkViewController: ViewController {
loading = true
let fakeUser = TelegramUser(id: EnginePeer.Id(namespace: .max, id: EnginePeer.Id.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [])
for i in 0 ..< count {
entries.append(.importer(Int32(i), presentationData.theme, presentationData.dateTimeFormat, EnginePeer.user(fakeUser), 0, true))
entries.append(.importer(Int32(i), presentationData.theme, presentationData.dateTimeFormat, EnginePeer.user(fakeUser), 0, false, true))
}
} else {
count = min(4, Int32(state.importers.count))
loading = false
for importer in state.importers {
if let peer = importer.peer.peer {
entries.append(.importer(index, presentationData.theme, presentationData.dateTimeFormat, EnginePeer(peer), importer.date, false))
entries.append(.importer(index, presentationData.theme, presentationData.dateTimeFormat, EnginePeer(peer), importer.date, importer.joinedViaFolderLink, false))
}
index += 1
}

View File

@ -38,6 +38,7 @@ public class ItemListFolderInviteLinkListItem: ListViewItem, ItemListItem {
public let sectionId: ItemListSectionId
let style: ItemListStyle
let tapAction: ((ExportedChatFolderLink) -> Void)?
let removeAction: ((ExportedChatFolderLink) -> Void)?
let contextAction: ((ExportedChatFolderLink, ASDisplayNode, ContextGesture?) -> Void)?
public let tag: ItemListItemTag?
@ -48,6 +49,7 @@ public class ItemListFolderInviteLinkListItem: ListViewItem, ItemListItem {
sectionId: ItemListSectionId,
style: ItemListStyle,
tapAction: ((ExportedChatFolderLink) -> Void)?,
removeAction: ((ExportedChatFolderLink) -> Void)?,
contextAction: ((ExportedChatFolderLink, ASDisplayNode, ContextGesture?) -> Void)?,
tag: ItemListItemTag? = nil
) {
@ -57,6 +59,7 @@ public class ItemListFolderInviteLinkListItem: ListViewItem, ItemListItem {
self.sectionId = sectionId
self.style = style
self.tapAction = tapAction
self.removeAction = removeAction
self.contextAction = contextAction
self.tag = tag
}
@ -125,7 +128,7 @@ public class ItemListFolderInviteLinkListItem: ListViewItem, ItemListItem {
}
}
public class ItemListFolderInviteLinkListItemNode: ListViewItemNode, ItemListItemNode {
public class ItemListFolderInviteLinkListItemNode: ItemListRevealOptionsItemNode, ItemListItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
@ -506,6 +509,15 @@ public class ItemListFolderInviteLinkListItemNode: ListViewItemNode, ItemListIte
strongSelf.placeholderNode = nil
shimmerNode.removeFromSupernode()
}
strongSelf.updateLayout(size: layout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
if item.removeAction != nil {
//TODO:localize
strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: "Delete", icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)]))
} else {
strongSelf.setRevealOptions((left: [], right: []))
}
}
})
}
@ -565,6 +577,21 @@ public class ItemListFolderInviteLinkListItemNode: ListViewItemNode, ItemListIte
shimmerNode.updateAbsoluteRect(rect, within: containerSize)
}
}
override public func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
super.updateRevealOffset(offset: offset, transition: transition)
transition.updateSublayerTransformOffset(layer: self.containerNode.layer, offset: CGPoint(x: offset, y: 0.0))
}
override public func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) {
if let item = self.layoutParams?.0, let invite = item.invite {
item.removeAction?(invite)
}
self.setRevealOptionsOpened(false, animated: true)
self.revealOptionsInteractivelyClosed()
}
}
private struct ContentParticle {

View File

@ -541,7 +541,7 @@ public func presentCustomNotificationSoundFilePicker(context: AccountContext, co
let data = try Data(contentsOf: url)
if data.count > settings.maxSize {
presentUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLarge_Title, text: presentationData.strings.Notifications_UploadError_TooLarge_Text(dataSizeString(Int64(settings.maxSize), formatting: DataSizeStringFormatting(presentationData: presentationData))).string))
presentUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLarge_Title, text: presentationData.strings.Notifications_UploadError_TooLarge_Text(dataSizeString(Int64(settings.maxSize), formatting: DataSizeStringFormatting(presentationData: presentationData))).string, timeout: nil))
souceUrl.stopAccessingSecurityScopedResource()
TempBox.shared.dispose(tempFile)
@ -594,7 +594,7 @@ public func presentCustomNotificationSoundFilePicker(context: AccountContext, co
if duration > Double(settings.maxDuration) {
souceUrl.stopAccessingSecurityScopedResource()
presentUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLong_Title(fileName).string, text: presentationData.strings.Notifications_UploadError_TooLong_Text(stringForDuration(Int32(settings.maxDuration))).string))
presentUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLong_Title(fileName).string, text: presentationData.strings.Notifications_UploadError_TooLong_Text(stringForDuration(Int32(settings.maxDuration))).string, timeout: nil))
} else {
Logger.shared.log("NotificationSoundSelection", "Uploading sound")

View File

@ -150,6 +150,7 @@ private final class PromptInputFieldNode: ASDisplayNode, ASEditableTextNodeDeleg
private final class PromptAlertContentNode: AlertContentNode {
private let strings: PresentationStrings
private let text: String
private let titleFont: PromptControllerTitleFont
private let textNode: ASTextNode
let inputFieldNode: PromptInputFieldNode
@ -174,9 +175,10 @@ private final class PromptAlertContentNode: AlertContentNode {
return self.isUserInteractionEnabled
}
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], text: String, value: String?) {
init(theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, actions: [TextAlertAction], text: String, titleFont: PromptControllerTitleFont, value: String?) {
self.strings = strings
self.text = text
self.titleFont = titleFont
self.textNode = ASTextNode()
self.textNode.maximumNumberOfLines = 2
@ -244,7 +246,14 @@ private final class PromptAlertContentNode: AlertContentNode {
}
override func updateTheme(_ theme: AlertControllerTheme) {
self.textNode.attributedText = NSAttributedString(string: self.text, font: Font.regular(13.0), textColor: theme.primaryColor, paragraphAlignment: .center)
let titleFontValue: UIFont
switch self.titleFont {
case .regular:
titleFontValue = Font.regular(13.0)
case .bold:
titleFontValue = Font.semibold(17.0)
}
self.textNode.attributedText = NSAttributedString(string: self.text, font: titleFontValue, textColor: theme.primaryColor, paragraphAlignment: .center)
self.actionNodesSeparator.backgroundColor = theme.separatorColor
for actionNode in self.actionNodes {
@ -379,7 +388,12 @@ private final class PromptAlertContentNode: AlertContentNode {
}
}
public func promptController(sharedContext: SharedAccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, text: String, value: String?, apply: @escaping (String?) -> Void) -> AlertController {
public enum PromptControllerTitleFont {
case regular
case bold
}
public func promptController(sharedContext: SharedAccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, text: String, titleFont: PromptControllerTitleFont = .regular, value: String?, apply: @escaping (String?) -> Void) -> AlertController {
let presentationData = updatedPresentationData?.initial ?? sharedContext.currentPresentationData.with { $0 }
var dismissImpl: ((Bool) -> Void)?
@ -393,7 +407,7 @@ public func promptController(sharedContext: SharedAccountContext, updatedPresent
applyImpl?()
})]
let contentNode = PromptAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, text: text, value: value)
let contentNode = PromptAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: presentationData.strings, actions: actions, text: text, titleFont: titleFont, value: value)
contentNode.complete = {
applyImpl?()
}

View File

@ -238,6 +238,10 @@ public final class QrCodeScreen: ViewController {
case let .invite(_, isGroup):
title = self.presentationData.strings.InviteLink_QRCode_Title
text = isGroup ? self.presentationData.strings.InviteLink_QRCode_Info : self.presentationData.strings.InviteLink_QRCode_InfoChannel
case .chatFolder:
//TODO:localize
title = "Invite by QR Code"
text = "Everyone on Telegram can scan this code to join your folder."
default:
title = ""
text = ""

View File

@ -470,7 +470,7 @@ func deleteAccountDataController(context: AccountContext, mode: DeleteAccountDat
let presentGlobalController = context.sharedContext.presentGlobalController
let _ = logoutFromAccount(id: accountId, accountManager: accountManager, alreadyLoggedOutRemotely: false).start(completed: {
Queue.mainQueue().after(0.1) {
presentGlobalController(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.DeleteAccount_Success), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), nil)
presentGlobalController(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.DeleteAccount_Success, timeout: nil), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return false }), nil)
}
})
})

View File

@ -920,7 +920,7 @@ public func privacyAndSecurityController(
hapticFeedback.impact()
var alreadyPresented = false
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Privacy_VoiceMessages_Tooltip), elevatedLayout: false, animateInAsReplacement: false, action: { action in
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Privacy_VoiceMessages_Tooltip, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { action in
if action == .info {
if !alreadyPresented {
let controller = PremiumIntroScreen(context: context, source: .settings)

View File

@ -1487,7 +1487,7 @@ public final class SolidRoundedButtonView: UIView {
badgeNode = current
} else {
badgeNode = BadgeNode(fillColor: self.theme.foregroundColor, strokeColor: .clear, textColor: self.theme.backgroundColor)
badgeNode.alpha = self.titleNode.alpha
badgeNode.alpha = self.titleNode.alpha == 0.0 ? 0.0 : 1.0
self.badgeNode = badgeNode
self.addSubnode(badgeNode)
}

View File

@ -118,7 +118,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1091179342] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionExportedInviteRevoke($0) }
dict[-484690728] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantInvite($0) }
dict[405815507] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantJoin($0) }
dict[1557846647] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantJoinByInvite($0) }
dict[-23084712] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantJoinByInvite($0) }
dict[-1347021750] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantJoinByRequest($0) }
dict[-124291086] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantLeave($0) }
dict[-115071790] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionParticipantMute($0) }

View File

@ -700,7 +700,7 @@ public extension Api {
case channelAdminLogEventActionExportedInviteRevoke(invite: Api.ExportedChatInvite)
case channelAdminLogEventActionParticipantInvite(participant: Api.ChannelParticipant)
case channelAdminLogEventActionParticipantJoin
case channelAdminLogEventActionParticipantJoinByInvite(invite: Api.ExportedChatInvite)
case channelAdminLogEventActionParticipantJoinByInvite(flags: Int32, invite: Api.ExportedChatInvite)
case channelAdminLogEventActionParticipantJoinByRequest(invite: Api.ExportedChatInvite, approvedBy: Int64)
case channelAdminLogEventActionParticipantLeave
case channelAdminLogEventActionParticipantMute(participant: Api.GroupCallParticipant)
@ -878,10 +878,11 @@ public extension Api {
}
break
case .channelAdminLogEventActionParticipantJoinByInvite(let invite):
case .channelAdminLogEventActionParticipantJoinByInvite(let flags, let invite):
if boxed {
buffer.appendInt32(1557846647)
buffer.appendInt32(-23084712)
}
serializeInt32(flags, buffer: buffer, boxed: false)
invite.serialize(buffer, true)
break
case .channelAdminLogEventActionParticipantJoinByRequest(let invite, let approvedBy):
@ -1059,8 +1060,8 @@ public extension Api {
return ("channelAdminLogEventActionParticipantInvite", [("participant", participant as Any)])
case .channelAdminLogEventActionParticipantJoin:
return ("channelAdminLogEventActionParticipantJoin", [])
case .channelAdminLogEventActionParticipantJoinByInvite(let invite):
return ("channelAdminLogEventActionParticipantJoinByInvite", [("invite", invite as Any)])
case .channelAdminLogEventActionParticipantJoinByInvite(let flags, let invite):
return ("channelAdminLogEventActionParticipantJoinByInvite", [("flags", flags as Any), ("invite", invite as Any)])
case .channelAdminLogEventActionParticipantJoinByRequest(let invite, let approvedBy):
return ("channelAdminLogEventActionParticipantJoinByRequest", [("invite", invite as Any), ("approvedBy", approvedBy as Any)])
case .channelAdminLogEventActionParticipantLeave:
@ -1431,13 +1432,16 @@ public extension Api {
return Api.ChannelAdminLogEventAction.channelAdminLogEventActionParticipantJoin
}
public static func parse_channelAdminLogEventActionParticipantJoinByInvite(_ reader: BufferReader) -> ChannelAdminLogEventAction? {
var _1: Api.ExportedChatInvite?
var _1: Int32?
_1 = reader.readInt32()
var _2: Api.ExportedChatInvite?
if let signature = reader.readInt32() {
_1 = Api.parse(reader, signature: signature) as? Api.ExportedChatInvite
_2 = Api.parse(reader, signature: signature) as? Api.ExportedChatInvite
}
let _c1 = _1 != nil
if _c1 {
return Api.ChannelAdminLogEventAction.channelAdminLogEventActionParticipantJoinByInvite(invite: _1!)
let _c2 = _2 != nil
if _c1 && _c2 {
return Api.ChannelAdminLogEventAction.channelAdminLogEventActionParticipantJoinByInvite(flags: _1!, invite: _2!)
}
else {
return nil

View File

@ -1574,7 +1574,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController
}).start()
}
strongSelf.presentUndoOverlay(content: .info(title: nil, text: strongSelf.presentationData.strings.VoiceChat_EditBioSuccess), action: { _ in return false })
strongSelf.presentUndoOverlay(content: .info(title: nil, text: strongSelf.presentationData.strings.VoiceChat_EditBioSuccess, timeout: nil), action: { _ in return false })
}
})
self?.controller?.present(controller, in: .window(.root))
@ -1595,7 +1595,7 @@ public final class VoiceChatControllerImpl: ViewController, VoiceChatController
if let strongSelf = self, let (firstName, lastName) = firstAndLastName {
let _ = context.engine.accountData.updateAccountPeerName(firstName: firstName, lastName: lastName).start()
strongSelf.presentUndoOverlay(content: .info(title: nil, text: strongSelf.presentationData.strings.VoiceChat_EditNameSuccess), action: { _ in return false })
strongSelf.presentUndoOverlay(content: .info(title: nil, text: strongSelf.presentationData.strings.VoiceChat_EditNameSuccess, timeout: nil), action: { _ in return false })
}
})
self?.controller?.present(controller, in: .window(.root))

View File

@ -73,7 +73,7 @@ public enum AdminLogEventAction {
case deleteExportedInvitation(ExportedInvitation)
case revokeExportedInvitation(ExportedInvitation)
case editExportedInvitation(previous: ExportedInvitation, updated: ExportedInvitation)
case participantJoinedViaInvite(ExportedInvitation)
case participantJoinedViaInvite(invitation: ExportedInvitation, joinedViaFolderLink: Bool)
case changeHistoryTTL(previousValue: Int32?, updatedValue: Int32?)
case changeTheme(previous: String?, updated: String?)
case participantJoinByRequest(invitation: ExportedInvitation, approvedBy: PeerId)
@ -268,8 +268,8 @@ func channelAdminLogEvents(postbox: Postbox, network: Network, peerId: PeerId, m
action = .revokeExportedInvitation(ExportedInvitation(apiExportedInvite: invite))
case let .channelAdminLogEventActionExportedInviteEdit(prevInvite, newInvite):
action = .editExportedInvitation(previous: ExportedInvitation(apiExportedInvite: prevInvite), updated: ExportedInvitation(apiExportedInvite: newInvite))
case let .channelAdminLogEventActionParticipantJoinByInvite(invite):
action = .participantJoinedViaInvite(ExportedInvitation(apiExportedInvite: invite))
case let .channelAdminLogEventActionParticipantJoinByInvite(flags, invite):
action = .participantJoinedViaInvite(invitation: ExportedInvitation(apiExportedInvite: invite), joinedViaFolderLink: (flags & (1 << 0)) != 0)
case let .channelAdminLogEventActionParticipantVolume(participant):
let parsedParticipant = GroupCallParticipantsContext.Update.StateUpdate.ParticipantUpdate(participant)
action = .groupCallUpdateParticipantVolume(peerId: parsedParticipant.peerId, volume: parsedParticipant.volume ?? 10000)

View File

@ -389,6 +389,20 @@ extension ChatListFilter {
case .allChats:
return nil
case let .filter(id, title, emoticon, data):
if data.isShared {
var flags: Int32 = 0
if emoticon != nil {
flags |= 1 << 25
}
return .dialogFilterCommunity(flags: flags, id: id, title: title, emoticon: emoticon, pinnedPeers: data.includePeers.pinnedPeers.compactMap { peerId -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}, includePeers: data.includePeers.peers.compactMap { peerId -> Api.InputPeer? in
if data.includePeers.pinnedPeers.contains(peerId) {
return nil
}
return transaction.getPeer(peerId).flatMap(apiInputPeer)
})
} else {
var flags: Int32 = 0
if data.excludeMuted {
flags |= 1 << 11
@ -413,6 +427,7 @@ extension ChatListFilter {
}, excludePeers: data.excludePeers.compactMap { peerId -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
})
}
}
}
}
@ -873,14 +888,21 @@ private func loadAndStorePeerChatInfos(accountPeerId: PeerId, postbox: Postbox,
struct ChatListFiltersState: Codable, Equatable {
struct ChatListFilterUpdates: Codable, Equatable {
struct MemberCount: Codable, Equatable {
var id: PeerId
var count: Int32
}
var folderId: Int32
var timestamp: Int32
var peerIds: [PeerId]
var memberCounts: [MemberCount]
init(folderId: Int32, timestamp: Int32, peerIds: [PeerId]) {
init(folderId: Int32, timestamp: Int32, peerIds: [PeerId], memberCounts: [MemberCount]) {
self.folderId = folderId
self.timestamp = timestamp
self.peerIds = peerIds
self.memberCounts = memberCounts
}
}

View File

@ -7,7 +7,7 @@ public func canShareLinkToPeer(peer: EnginePeer) -> Bool {
var isEnabled = false
switch peer {
case let .channel(channel):
if channel.adminRights != nil && channel.hasPermission(.inviteMembers) {
if channel.flags.contains(.isCreator) || (channel.adminRights?.rights.contains(.canInviteUsers) == true) {
isEnabled = true
} else if channel.username != nil {
isEnabled = true
@ -24,6 +24,7 @@ public func canShareLinkToPeer(peer: EnginePeer) -> Bool {
public enum ExportChatFolderError {
case generic
case sharedFolderLimitExceeded(limit: Int32, premiumLimit: Int32)
case limitExceeded(limit: Int32, premiumLimit: Int32)
}
@ -75,9 +76,9 @@ func _internal_exportChatFolder(account: Account, filterId: Int32, title: String
if error.errorDescription == "COMMUNITIES_TOO_MUCH" {
if isPremium {
return .fail(.limitExceeded(limit: userPremiumLimits.maxSharedFolderJoin, premiumLimit: userPremiumLimits.maxSharedFolderJoin))
return .fail(.sharedFolderLimitExceeded(limit: userPremiumLimits.maxSharedFolderJoin, premiumLimit: userPremiumLimits.maxSharedFolderJoin))
} else {
return .fail(.limitExceeded(limit: userDefaultLimits.maxSharedFolderJoin, premiumLimit: userPremiumLimits.maxSharedFolderJoin))
return .fail(.sharedFolderLimitExceeded(limit: userDefaultLimits.maxSharedFolderJoin, premiumLimit: userPremiumLimits.maxSharedFolderJoin))
}
} else {
if isPremium {
@ -236,17 +237,20 @@ public final class ChatFolderLinkContents {
public let title: String?
public let peers: [EnginePeer]
public let alreadyMemberPeerIds: Set<EnginePeer.Id>
public let memberCounts: [EnginePeer.Id: Int]
public init(
localFilterId: Int32?,
title: String?,
peers: [EnginePeer],
alreadyMemberPeerIds: Set<EnginePeer.Id>
alreadyMemberPeerIds: Set<EnginePeer.Id>,
memberCounts: [EnginePeer.Id: Int]
) {
self.localFilterId = localFilterId
self.title = title
self.peers = peers
self.alreadyMemberPeerIds = alreadyMemberPeerIds
self.memberCounts = memberCounts
}
}
@ -263,6 +267,7 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal<Cha
var allPeers: [Peer] = []
var peerPresences: [PeerId: Api.User] = [:]
var memberCounts: [PeerId: Int] = [:]
for user in users {
let telegramUser = TelegramUser(user: user)
@ -273,6 +278,11 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal<Cha
if let peer = parseTelegramGroupOrChannel(chat: chat) {
allPeers.append(peer)
}
if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _) = chat {
if let participantsCount = participantsCount {
memberCounts[chat.peerId] = Int(participantsCount)
}
}
}
updatePeers(transaction: transaction, peers: allPeers, update: { _, updated -> Peer in
@ -293,12 +303,13 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal<Cha
}
alreadyMemberPeerIds.removeAll()
return ChatFolderLinkContents(localFilterId: nil, title: title, peers: resultPeers, alreadyMemberPeerIds: alreadyMemberPeerIds)
return ChatFolderLinkContents(localFilterId: nil, title: title, peers: resultPeers, alreadyMemberPeerIds: alreadyMemberPeerIds, memberCounts: memberCounts)
case let .communityInviteAlready(filterId, missingPeers, alreadyPeers, chats, users):
let _ = alreadyPeers
var allPeers: [Peer] = []
var peerPresences: [PeerId: Api.User] = [:]
var memberCounts: [PeerId: Int] = [:]
for user in users {
let telegramUser = TelegramUser(user: user)
@ -309,6 +320,11 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal<Cha
if let peer = parseTelegramGroupOrChannel(chat: chat) {
allPeers.append(peer)
}
if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _) = chat {
if let participantsCount = participantsCount {
memberCounts[chat.peerId] = Int(participantsCount)
}
}
}
updatePeers(transaction: transaction, peers: allPeers, update: { _, updated -> Peer in
@ -355,7 +371,7 @@ func _internal_checkChatFolderLink(account: Account, slug: String) -> Signal<Cha
}
}
return ChatFolderLinkContents(localFilterId: filterId, title: currentFilterTitle, peers: resultPeers, alreadyMemberPeerIds: alreadyMemberPeerIds)
return ChatFolderLinkContents(localFilterId: filterId, title: currentFilterTitle, peers: resultPeers, alreadyMemberPeerIds: alreadyMemberPeerIds, memberCounts: memberCounts)
}
}
|> castError(CheckChatFolderLinkError.self)
@ -369,12 +385,31 @@ public enum JoinChatFolderLinkError {
case tooManyChannels(limit: Int32, premiumLimit: Int32)
}
func _internal_joinChatFolderLink(account: Account, slug: String, peerIds: [EnginePeer.Id]) -> Signal<Never, JoinChatFolderLinkError> {
return account.postbox.transaction { transaction -> [Api.InputPeer] in
return peerIds.compactMap(transaction.getPeer).compactMap(apiInputPeer)
public final class JoinChatFolderResult {
public let folderId: Int32
public let title: String
public let newChatCount: Int
public init(folderId: Int32, title: String, newChatCount: Int) {
self.folderId = folderId
self.title = title
self.newChatCount = newChatCount
}
}
func _internal_joinChatFolderLink(account: Account, slug: String, peerIds: [EnginePeer.Id]) -> Signal<JoinChatFolderResult, JoinChatFolderLinkError> {
return account.postbox.transaction { transaction -> ([Api.InputPeer], Int) in
var newChatCount = 0
for peerId in peerIds {
if transaction.getPeerChatListIndex(peerId) != nil {
newChatCount += 1
}
}
return (peerIds.compactMap(transaction.getPeer).compactMap(apiInputPeer), newChatCount)
}
|> castError(JoinChatFolderLinkError.self)
|> mapToSignal { inputPeers -> Signal<Never, JoinChatFolderLinkError> in
|> mapToSignal { inputPeers, newChatCount -> Signal<JoinChatFolderResult, JoinChatFolderLinkError> in
return account.network.request(Api.functions.communities.joinCommunityInvite(slug: slug, peers: inputPeers))
|> `catch` { error -> Signal<Api.Updates, JoinChatFolderLinkError> in
if error.errorDescription == "USER_CHANNELS_TOO_MUCH" {
@ -426,10 +461,36 @@ func _internal_joinChatFolderLink(account: Account, slug: String, peerIds: [Engi
return .fail(.generic)
}
}
|> mapToSignal { result -> Signal<Never, JoinChatFolderLinkError> in
|> mapToSignal { result -> Signal<JoinChatFolderResult, JoinChatFolderLinkError> in
account.stateManager.addUpdates(result)
return .complete()
var folderResult: JoinChatFolderResult?
for update in result.allUpdates {
if case let .updateDialogFilter(_, id, data) = update {
if let data = data, case let .filter(_, title, _, _) = ChatListFilter(apiFilter: data) {
folderResult = JoinChatFolderResult(folderId: id, title: title, newChatCount: newChatCount)
}
break
}
}
if let folderResult = folderResult {
return _internal_updatedChatListFilters(postbox: account.postbox)
|> castError(JoinChatFolderLinkError.self)
|> filter { filters -> Bool in
if filters.contains(where: { $0.id == folderResult.folderId }) {
return true
} else {
return false
}
}
|> take(1)
|> map { _ -> JoinChatFolderResult in
return folderResult
}
} else {
return .fail(.generic)
}
}
}
}
@ -438,23 +499,26 @@ public final class ChatFolderUpdates: Equatable {
fileprivate let folderId: Int32
fileprivate let title: String
fileprivate let missingPeers: [EnginePeer]
fileprivate let memberCounts: [EnginePeer.Id: Int]
public var availableChatsToJoin: Int {
return self.missingPeers.count
}
public var chatFolderLinkContents: ChatFolderLinkContents {
return ChatFolderLinkContents(localFilterId: self.folderId, title: self.title, peers: self.missingPeers, alreadyMemberPeerIds: Set())
return ChatFolderLinkContents(localFilterId: self.folderId, title: self.title, peers: self.missingPeers, alreadyMemberPeerIds: Set(), memberCounts: self.memberCounts)
}
fileprivate init(
folderId: Int32,
title: String,
missingPeers: [EnginePeer]
missingPeers: [EnginePeer],
memberCounts: [EnginePeer.Id: Int]
) {
self.folderId = folderId
self.title = title
self.missingPeers = missingPeers
self.memberCounts = memberCounts
}
public static func ==(lhs: ChatFolderUpdates, rhs: ChatFolderUpdates) -> Bool {
@ -512,7 +576,7 @@ func _internal_pollChatFolderUpdatesOnce(account: Account, folderId: Int32) -> S
var state = state
state.updates.removeAll(where: { $0.folderId == folderId })
state.updates.append(ChatListFiltersState.ChatListFilterUpdates(folderId: folderId, timestamp: Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970), peerIds: []))
state.updates.append(ChatListFiltersState.ChatListFilterUpdates(folderId: folderId, timestamp: Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970), peerIds: [], memberCounts: []))
return state
})
@ -524,6 +588,7 @@ func _internal_pollChatFolderUpdatesOnce(account: Account, folderId: Int32) -> S
return account.postbox.transaction { transaction -> Void in
var peers: [Peer] = []
var peerPresences: [PeerId: Api.User] = [:]
var memberCounts: [ChatListFiltersState.ChatListFilterUpdates.MemberCount] = []
for user in users {
let telegramUser = TelegramUser(user: user)
@ -534,6 +599,11 @@ func _internal_pollChatFolderUpdatesOnce(account: Account, folderId: Int32) -> S
if let peer = parseTelegramGroupOrChannel(chat: chat) {
peers.append(peer)
}
if case let .channel(_, _, _, _, _, _, _, _, _, _, _, _, participantsCount, _) = chat {
if let participantsCount = participantsCount {
memberCounts.append(ChatListFiltersState.ChatListFilterUpdates.MemberCount(id: chat.peerId, count: participantsCount))
}
}
}
updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in
@ -545,7 +615,7 @@ func _internal_pollChatFolderUpdatesOnce(account: Account, folderId: Int32) -> S
var state = state
state.updates.removeAll(where: { $0.folderId == folderId })
state.updates.append(ChatListFiltersState.ChatListFilterUpdates(folderId: folderId, timestamp: Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970), peerIds: missingPeers.map(\.peerId)))
state.updates.append(ChatListFiltersState.ChatListFilterUpdates(folderId: folderId, timestamp: Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970), peerIds: missingPeers.map(\.peerId), memberCounts: memberCounts))
return state
})
@ -560,6 +630,7 @@ func _internal_subscribedChatFolderUpdates(account: Account, folderId: Int32) ->
struct InternalData: Equatable {
var title: String
var peerIds: [EnginePeer.Id]
var memberCounts: [EnginePeer.Id: Int]
}
return _internal_updatedChatListFiltersState(postbox: account.postbox)
@ -574,7 +645,11 @@ func _internal_subscribedChatFolderUpdates(account: Account, folderId: Int32) ->
return nil
}
let filteredPeerIds: [PeerId] = update.peerIds.filter { !data.includePeers.peers.contains($0) }
return InternalData(title: title, peerIds: filteredPeerIds)
var memberCounts: [PeerId: Int] = [:]
for item in update.memberCounts {
memberCounts[item.id] = Int(item.count)
}
return InternalData(title: title, peerIds: filteredPeerIds, memberCounts: memberCounts)
}
|> distinctUntilChanged
|> mapToSignal { internalData -> Signal<ChatFolderUpdates?, NoError> in
@ -591,7 +666,7 @@ func _internal_subscribedChatFolderUpdates(account: Account, folderId: Int32) ->
peers.append(EnginePeer(peer))
}
}
return ChatFolderUpdates(folderId: folderId, title: internalData.title, missingPeers: peers)
return ChatFolderUpdates(folderId: folderId, title: internalData.title, missingPeers: peers, memberCounts: internalData.memberCounts)
}
}
}
@ -685,3 +760,13 @@ func _internal_leaveChatFolder(account: Account, folderId: Int32, removePeerIds:
}
}
}
func _internal_requestLeaveChatFolderSuggestions(account: Account, folderId: Int32) -> Signal<[EnginePeer.Id], NoError> {
return account.network.request(Api.functions.communities.getLeaveCommunitySuggestions(community: .inputCommunityDialogFilter(filterId: folderId)))
|> map { result -> [EnginePeer.Id] in
return result.map(\.peerId)
}
|> `catch` { _ -> Signal<[EnginePeer.Id], NoError> in
return .single([])
}
}

View File

@ -664,6 +664,7 @@ public struct PeerInvitationImportersState: Equatable {
public var date: Int32
public var about: String?
public var approvedBy: PeerId?
public var joinedViaFolderLink: Bool
}
public var importers: [Importer]
public var isLoadingMore: Bool
@ -708,6 +709,7 @@ final class CachedPeerInvitationImporters: Codable {
let peerIds: [PeerId]
let dates: [PeerId: Int32]
let abouts: [PeerId: String]
let joinedViaFolderLink: [PeerId: Bool]
let count: Int32
static func key(peerId: PeerId, link: String, requested: Bool) -> ValueBoxKey {
@ -727,13 +729,17 @@ final class CachedPeerInvitationImporters: Codable {
$0[$1.peer.peerId] = about
}
}
self.joinedViaFolderLink = importers.reduce(into: [PeerId: Bool]()) {
$0[$1.peer.peerId] = $1.joinedViaFolderLink
}
self.count = count
}
init(peerIds: [PeerId], dates: [PeerId: Int32], abouts: [PeerId: String], count: Int32) {
init(peerIds: [PeerId], dates: [PeerId: Int32], abouts: [PeerId: String], joinedViaFolderLink: [PeerId: Bool], count: Int32) {
self.peerIds = peerIds
self.dates = dates
self.abouts = abouts
self.joinedViaFolderLink = joinedViaFolderLink
self.count = count
}
@ -752,6 +758,16 @@ final class CachedPeerInvitationImporters: Codable {
}
self.dates = dates
var joinedViaFolderLink: [PeerId: Bool] = [:]
let joinedViaFolderLinkArray = try container.decode([Int64].self, forKey: "joinedViaFolderLink")
for index in stride(from: 0, to: joinedViaFolderLinkArray.endIndex, by: 2) {
let userId = datesArray[index]
let value = datesArray[index + 1]
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))
joinedViaFolderLink[peerId] = value != 0
}
self.joinedViaFolderLink = joinedViaFolderLink
var abouts: [PeerId: String] = [:]
let aboutsArray = try container.decodeIfPresent([DictionaryPair].self, forKey: "abouts")
if let aboutsArray = aboutsArray {
@ -777,6 +793,13 @@ final class CachedPeerInvitationImporters: Codable {
}
try container.encode(dates, forKey: "dates")
var joinedViaFolderLink: [Int64] = []
for (peerId, value) in self.joinedViaFolderLink {
joinedViaFolderLink.append(peerId.id._internalGetInt64Value())
joinedViaFolderLink.append(Int64(value ? 1 : 0))
}
try container.encode(joinedViaFolderLink, forKey: "joinedViaFolderLink")
var abouts: [DictionaryPair] = []
for (peerId, about) in self.abouts {
abouts.append(DictionaryPair(peerId.id._internalGetInt64Value(), value: about))
@ -847,7 +870,7 @@ private final class PeerInvitationImportersContextImpl {
var result: [PeerInvitationImportersState.Importer] = []
for peerId in cachedResult.peerIds {
if let peer = transaction.getPeer(peerId), let date = cachedResult.dates[peerId] {
result.append(PeerInvitationImportersState.Importer(peer: RenderedPeer(peer: peer), date: date, about: cachedResult.abouts[peerId]))
result.append(PeerInvitationImportersState.Importer(peer: RenderedPeer(peer: peer), date: date, about: cachedResult.abouts[peerId], joinedViaFolderLink: cachedResult.joinedViaFolderLink[peerId] ?? false))
} else {
return nil
}
@ -958,15 +981,17 @@ private final class PeerInvitationImportersContextImpl {
let date: Int32
let about: String?
let approvedBy: PeerId?
let joinedViaFolderLink: Bool
switch importer {
case let .chatInviteImporter(_, userId, dateValue, aboutValue, approvedByValue):
case let .chatInviteImporter(flags, userId, dateValue, aboutValue, approvedByValue):
joinedViaFolderLink = (flags & (1 << 3)) != 0
peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))
date = dateValue
about = aboutValue
approvedBy = approvedByValue.flatMap { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) }
}
if let peer = transaction.getPeer(peerId) {
resultImporters.append(PeerInvitationImportersState.Importer(peer: RenderedPeer(peer: peer), date: date, about: about, approvedBy: approvedBy))
resultImporters.append(PeerInvitationImportersState.Importer(peer: RenderedPeer(peer: peer), date: date, about: about, approvedBy: approvedBy, joinedViaFolderLink: joinedViaFolderLink))
}
}
if populateCache && query == nil {

View File

@ -1054,7 +1054,7 @@ public extension TelegramEngine {
return _internal_checkChatFolderLink(account: self.account, slug: slug)
}
public func joinChatFolderLink(slug: String, peerIds: [EnginePeer.Id]) -> Signal<Never, JoinChatFolderLinkError> {
public func joinChatFolderLink(slug: String, peerIds: [EnginePeer.Id]) -> Signal<JoinChatFolderResult, JoinChatFolderLinkError> {
return _internal_joinChatFolderLink(account: self.account, slug: slug, peerIds: peerIds)
}
@ -1085,6 +1085,10 @@ public extension TelegramEngine {
public func leaveChatFolder(folderId: Int32, removePeerIds: [EnginePeer.Id]) -> Signal<Never, NoError> {
return _internal_leaveChatFolder(account: self.account, folderId: folderId, removePeerIds: removePeerIds)
}
public func requestLeaveChatFolderSuggestions(folderId: Int32) -> Signal<[EnginePeer.Id], NoError> {
return _internal_requestLeaveChatFolderSuggestions(account: self.account, folderId: folderId)
}
}
}

View File

@ -55,12 +55,14 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
var containerInset: CGFloat
var bottomInset: CGFloat
var topInset: CGFloat
var contentHeight: CGFloat
init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat) {
init(containerSize: CGSize, containerInset: CGFloat, bottomInset: CGFloat, topInset: CGFloat, contentHeight: CGFloat) {
self.containerSize = containerSize
self.containerInset = containerInset
self.bottomInset = bottomInset
self.topInset = topInset
self.contentHeight = contentHeight
}
}
@ -82,6 +84,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
private let scrollView: ScrollView
private let scrollContentClippingView: SparseContainerView
private let scrollContentView: UIView
private let bottomBackgroundLayer: SimpleLayer
private let bottomSeparatorLayer: SimpleLayer
private let topIcon = ComponentView<Empty>()
@ -134,6 +138,9 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
self.itemContainerView.clipsToBounds = true
self.itemContainerView.layer.cornerRadius = 10.0
self.bottomBackgroundLayer = SimpleLayer()
self.bottomSeparatorLayer = SimpleLayer()
super.init(frame: frame)
self.addSubview(self.dimView)
@ -165,6 +172,9 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
self.scrollContentView.addSubview(self.itemContainerView)
self.layer.addSublayer(self.bottomBackgroundLayer)
self.layer.addSublayer(self.bottomSeparatorLayer)
self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
}
@ -231,6 +241,15 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
topOffset = max(0.0, topOffset)
transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0))
let bottomDistance = itemLayout.contentHeight - self.scrollView.bounds.maxY
let bottomAlphaDistance: CGFloat = 30.0
var bottomAlpha: CGFloat = bottomDistance / bottomAlphaDistance
bottomAlpha = max(0.0, min(1.0, bottomAlpha))
let bottomOverlayAlpha: CGFloat = bottomAlpha
transition.setAlpha(layer: self.bottomBackgroundLayer, alpha: bottomOverlayAlpha)
transition.setAlpha(layer: self.bottomSeparatorLayer, alpha: bottomOverlayAlpha)
transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset))
let topOffsetDistance: CGFloat = min(200.0, floor(itemLayout.containerSize.height * 0.25))
@ -244,10 +263,12 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
func animateIn() {
self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
let animateOffset: CGFloat = self.backgroundLayer.frame.minY
let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY
self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.bottomBackgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.bottomSeparatorLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
if let actionButtonView = self.actionButton.view {
actionButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
}
@ -265,6 +286,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
completion()
})
self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
self.bottomBackgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
self.bottomSeparatorLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
if let actionButtonView = self.actionButton.view {
actionButtonView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
@ -287,8 +310,16 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
let sideInset: CGFloat = 16.0
if self.component?.linkContents == nil, let linkContents = component.linkContents {
for peer in linkContents.peers {
self.selectedItems.insert(peer.id)
if case let .remove(_, defaultSelectedPeerIds) = component.subject {
for peer in linkContents.peers {
if defaultSelectedPeerIds.contains(peer.id) {
self.selectedItems.insert(peer.id)
}
}
} else {
for peer in linkContents.peers {
self.selectedItems.insert(peer.id)
}
}
}
@ -300,6 +331,8 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
self.backgroundLayer.backgroundColor = environment.theme.list.blocksBackgroundColor.cgColor
self.itemContainerView.backgroundColor = environment.theme.list.itemBlocksBackgroundColor
self.bottomBackgroundLayer.backgroundColor = environment.theme.rootController.navigationBar.opaqueBackgroundColor.cgColor
self.bottomSeparatorLayer.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor
}
transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize))
@ -477,6 +510,13 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
self.items[id] = item
}
var subtitle: String?
if linkContents.alreadyMemberPeerIds.contains(peer.id) {
subtitle = "You are already a member"
} else if let memberCount = linkContents.memberCounts[peer.id] {
subtitle = "\(memberCount) participants"
}
let itemSize = item.update(
transition: itemTransition,
component: AnyComponent(PeerListItemComponent(
@ -486,7 +526,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
sideInset: 0.0,
title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
peer: peer,
subtitle: nil,
subtitle: subtitle,
selectionState: .editing(isSelected: self.selectedItems.contains(peer.id), isTinted: linkContents.alreadyMemberPeerIds.contains(peer.id)),
hasNext: i != linkContents.peers.count - 1,
action: { [weak self] peer in
@ -639,7 +679,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
self.selectedItems.insert(peerId)
}
}
self.state?.updated(transition: .immediate)
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut)))
}
)),
environment: {},
@ -712,7 +752,7 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
return
}
if case let .remove(folderId) = component.subject {
if case let .remove(folderId, _) = component.subject {
self.inProgress = true
self.state?.updated(transition: .immediate)
@ -729,21 +769,72 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
controller.dismiss()
} else if let _ = component.linkContents {
if self.joinDisposable == nil, !self.selectedItems.isEmpty {
let joinSignal: Signal<Never, JoinChatFolderLinkError>
let joinSignal: Signal<JoinChatFolderResult?, JoinChatFolderLinkError>
switch component.subject {
case .remove:
return
case let .slug(slug):
joinSignal = component.context.engine.peers.joinChatFolderLink(slug: slug, peerIds: Array(self.selectedItems))
|> map(Optional.init)
case let .updates(updates):
var result: JoinChatFolderResult?
if let localFilterId = updates.chatFolderLinkContents.localFilterId, let title = updates.chatFolderLinkContents.title {
result = JoinChatFolderResult(folderId: localFilterId, title: title, newChatCount: self.selectedItems.count)
}
joinSignal = component.context.engine.peers.joinAvailableChatsInFolder(updates: updates, peerIds: Array(self.selectedItems))
|> map { _ -> JoinChatFolderResult? in
}
|> then(Signal<JoinChatFolderResult?, JoinChatFolderLinkError>.single(result))
}
self.inProgress = true
self.state?.updated(transition: .immediate)
self.joinDisposable = (joinSignal
|> deliverOnMainQueue).start(error: { [weak self] error in
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let self, let component = self.component, let controller = self.environment?.controller() else {
return
}
if let result, let navigationController = controller.navigationController as? NavigationController {
var chatListController: ChatListController?
for viewController in navigationController.viewControllers {
if let rootController = viewController as? TabBarController {
for c in rootController.controllers {
if let c = c as? ChatListController {
chatListController = c
break
}
}
} else if let c = viewController as? ChatListController {
chatListController = c
break
}
}
if let chatListController {
navigationController.popToRoot(animated: true)
let context = component.context
chatListController.navigateToFolder(folderId: result.folderId, completion: { [weak context, weak chatListController] in
guard let context, let chatListController else {
return
}
//TODO:localize
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
if case .updates = component.subject {
chatListController.present(UndoOverlayController(presentationData: presentationData, content: .info(title: "Folder \(result.title) Updated", text: "You have joined \(result.newChatCount) new chats", timeout: nil), elevatedLayout: false, action: { _ in true }), in: .current)
} else if result.newChatCount != 0 {
chatListController.present(UndoOverlayController(presentationData: presentationData, content: .info(title: "Folder \(result.title) Added", text: "You also joined \(result.newChatCount) chats", timeout: nil), elevatedLayout: false, action: { _ in true }), in: .current)
} else {
chatListController.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: "Folder \(result.title) Added", timeout: nil), elevatedLayout: false, action: { _ in true }), in: .current)
}
})
}
}
controller.dismiss()
}, error: { [weak self] error in
guard let self, let component = self.component, let controller = self.environment?.controller() else {
return
}
@ -764,11 +855,6 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
controller.push(limitController)
controller.dismiss()
}
}, completed: { [weak self] in
guard let self, let controller = self.environment?.controller() else {
return
}
controller.dismiss()
})
} else {
controller.dismiss()
@ -788,6 +874,9 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
transition.setFrame(view: actionButtonView, frame: actionButtonFrame)
}
transition.setFrame(layer: self.bottomBackgroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0), size: CGSize(width: availableSize.width, height: bottomPanelHeight)))
transition.setFrame(layer: self.bottomSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0 - UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
if let controller = environment.controller() {
let subLayout = ContainerViewLayout(
size: availableSize, metrics: environment.metrics, deviceMetrics: environment.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: sideInset - 12.0, bottom: bottomPanelHeight, right: sideInset),
@ -809,16 +898,16 @@ private final class ChatFolderLinkPreviewScreenComponent: Component {
let scrollContentHeight = max(topInset + contentHeight, availableSize.height - containerInset)
self.scrollContentClippingView.layer.cornerRadius = 10.0
//self.scrollContentClippingView.layer.cornerRadius = 10.0
self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset)
self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: environment.safeInsets.bottom, topInset: topInset, contentHeight: scrollContentHeight)
transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight)))
transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0))
transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize))
let scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset + 56.0), size: CGSize(width: availableSize.width - sideInset * 2.0, height: actionButtonFrame.minY - 24.0 - (containerInset + 56.0)))
let scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset + 56.0), size: CGSize(width: availableSize.width - sideInset * 2.0, height: actionButtonFrame.minY - 8.0 - (containerInset + 56.0)))
transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center)
transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size))
@ -851,7 +940,7 @@ public class ChatFolderLinkPreviewScreen: ViewControllerComponentContainer {
public enum Subject: Equatable {
case slug(String)
case updates(ChatFolderUpdates)
case remove(folderId: Int32)
case remove(folderId: Int32, defaultSelectedPeerIds: [EnginePeer.Id])
}
private let context: AccountContext
@ -868,6 +957,7 @@ public class ChatFolderLinkPreviewScreen: ViewControllerComponentContainer {
self.navigationPresentation = .flatModal
self.blocksBackgroundWhenInOverlay = true
self.automaticallyControlPresentationContextLayout = false
self.lockOrientation = true
}
required public init(coder aDecoder: NSCoder) {

View File

@ -182,7 +182,7 @@ final class PeerListItemComponent: Component {
if themeUpdated {
var theme = CheckNodeTheme(theme: component.theme, style: .plain)
if isTinted {
theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.35)
theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.5)
}
checkLayer.theme = theme
}
@ -190,7 +190,7 @@ final class PeerListItemComponent: Component {
} else {
var theme = CheckNodeTheme(theme: component.theme, style: .plain)
if isTinted {
theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.35)
theme.backgroundColor = theme.backgroundColor.mixedWith(component.theme.list.itemBlocksBackgroundColor, alpha: 0.5)
}
checkLayer = CheckLayer(theme: theme)
self.checkLayer = checkLayer

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "link_18.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,101 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 2.304932 1.549805 cm
0.000000 0.000000 0.000000 scn
7.766377 12.077935 m
8.749319 13.060877 10.342982 13.060877 11.325925 12.077935 c
12.308867 11.094994 12.308867 9.501329 11.325925 8.518387 c
9.825925 7.018387 l
8.842983 6.035446 7.249319 6.035446 6.266377 7.018387 c
6.135469 7.149295 6.022435 7.290437 5.926912 7.438976 c
5.728258 7.747883 5.316799 7.837261 5.007892 7.638608 c
4.698985 7.439953 4.609607 7.028494 4.808261 6.719588 c
4.954801 6.491719 5.127476 6.276385 5.325925 6.077936 c
6.828264 4.575597 9.264038 4.575597 10.766377 6.077936 c
12.266377 7.577936 l
13.768716 9.080275 13.768716 11.516048 12.266377 13.018387 c
10.764037 14.520726 8.328264 14.520726 6.825925 13.018387 c
5.325925 11.518387 l
5.066226 11.258689 5.066226 10.837634 5.325925 10.577935 c
5.585624 10.318236 6.006679 10.318236 6.266377 10.577935 c
7.766377 12.077935 l
h
5.626755 2.818445 m
4.643812 1.835504 3.050149 1.835504 2.067207 2.818445 c
1.084265 3.801387 1.084265 5.395051 2.067207 6.377992 c
3.567207 7.877992 l
4.550149 8.860934 6.143812 8.860934 7.126754 7.877992 c
7.257662 7.747084 7.370696 7.605942 7.466219 7.457404 c
7.664873 7.148497 8.076332 7.059119 8.385240 7.257772 c
8.694146 7.456426 8.783525 7.867885 8.584870 8.176792 c
8.438332 8.404661 8.265656 8.619995 8.067206 8.818444 c
6.564867 10.320784 4.129094 10.320784 2.626755 8.818444 c
1.126755 7.318444 l
-0.375585 5.816106 -0.375585 3.380332 1.126755 1.877994 c
2.629094 0.375654 5.064867 0.375654 6.567206 1.877994 c
8.067206 3.377994 l
8.326905 3.637691 8.326905 4.058746 8.067206 4.318445 c
7.807508 4.578144 7.386453 4.578144 7.126754 4.318445 c
5.626755 2.818445 l
h
f*
n
Q
endstream
endobj
3 0 obj
1707
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 18.000000 18.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001797 00000 n
0000001820 00000 n
0000001993 00000 n
0000002067 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
2126
%%EOF

View File

@ -7330,7 +7330,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let strongSelf = self, case let .message(index) = toIndex {
if case let .message(messageSubject, _, _) = strongSelf.subject, initial, case let .id(messageId) = messageSubject, messageId != index.id {
if messageId.peerId == index.id.peerId {
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Conversation_MessageDoesntExist), elevatedLayout: false, action: { _ in return true }), in: .current)
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Conversation_MessageDoesntExist, timeout: nil), elevatedLayout: false, action: { _ in return true }), in: .current)
}
} else if let controllerInteraction = strongSelf.controllerInteraction {
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(index.id) {
@ -8809,12 +8809,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
bannedMediaInput = true
} else if channel.hasBannedPermission(.banSendVoice) != nil {
if !isVideo {
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText()))
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil))
return
}
} else if channel.hasBannedPermission(.banSendInstantVideos) != nil {
if isVideo {
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText()))
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil))
return
}
}
@ -8823,12 +8823,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
bannedMediaInput = true
} else if group.hasBannedPermission(.banSendVoice) {
if !isVideo {
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText()))
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil))
return
}
} else if group.hasBannedPermission(.banSendInstantVideos) {
if isVideo {
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText()))
strongSelf.controllerInteraction?.displayUndo(.info(title: nil, text: strongSelf.restrictedSendingContentsText(), timeout: nil))
return
}
}
@ -10130,7 +10130,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
presentAddMembersImpl(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, parentController: strongSelf, groupPeer: peer, selectAddMemberDisposable: strongSelf.selectAddMemberDisposable, addMemberDisposable: strongSelf.addMemberDisposable)
}, presentGigagroupHelp: { [weak self] in
if let strongSelf = self {
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Conversation_GigagroupDescription), elevatedLayout: false, action: { _ in return true }), in: .current)
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Conversation_GigagroupDescription, timeout: nil), elevatedLayout: false, action: { _ in return true }), in: .current)
}
}, editMessageMedia: { [weak self] messageId, draw in
if let strongSelf = self {
@ -11204,7 +11204,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in return nil }), textAlignment: .center)
let controller = richTextAlertController(context: strongSelf.context, title: attributedTitle, text: attributedText, actions: [TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.BroadcastGroups_LimitAlert_SettingsTip), elevatedLayout: false, action: { _ in return false }), in: .current)
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.BroadcastGroups_LimitAlert_SettingsTip, timeout: nil), elevatedLayout: false, action: { _ in return false }), in: .current)
}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.BroadcastGroups_LimitAlert_LearnMore, action: {
let context = strongSelf.context
@ -12997,7 +12997,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return false
}
}), case let .app(_, botName, _) = button {
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: botJustInstalled ? strongSelf.presentationData.strings.WebApp_AddToAttachmentSucceeded(botName).string : strongSelf.presentationData.strings.WebApp_AddToAttachmentAlreadyAddedError), elevatedLayout: false, action: { _ in return false }), in: .current)
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: botJustInstalled ? strongSelf.presentationData.strings.WebApp_AddToAttachmentSucceeded(botName).string : strongSelf.presentationData.strings.WebApp_AddToAttachmentAlreadyAddedError, timeout: nil), elevatedLayout: false, action: { _ in return false }), in: .current)
} else {
let _ = (context.engine.messages.getAttachMenuBot(botId: botId)
|> deliverOnMainQueue).start(next: { bot in

View File

@ -815,7 +815,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
let _ = context.engine.messages.rateAudioTranscription(messageId: message.id, id: audioTranscription.id, isGood: value).start()
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let content: UndoOverlayContent = .info(title: nil, text: presentationData.strings.Chat_AudioTranscriptionFeedbackTip)
let content: UndoOverlayContent = .info(title: nil, text: presentationData.strings.Chat_AudioTranscriptionFeedbackTip, timeout: nil)
controllerInteraction.displayUndo(content)
}), false), at: 0)
actions.insert(.separator, at: 1)
@ -842,9 +842,9 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
let settings = NotificationSoundSettings.extract(from: context.currentAppConfiguration.with({ $0 }))
if size > settings.maxSize {
controllerInteraction.displayUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLarge_Title, text: presentationData.strings.Notifications_UploadError_TooLarge_Text(dataSizeString(Int64(settings.maxSize), formatting: DataSizeStringFormatting(presentationData: presentationData))).string))
controllerInteraction.displayUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLarge_Title, text: presentationData.strings.Notifications_UploadError_TooLarge_Text(dataSizeString(Int64(settings.maxSize), formatting: DataSizeStringFormatting(presentationData: presentationData))).string, timeout: nil))
} else if Double(duration) > Double(settings.maxDuration) {
controllerInteraction.displayUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLong_Title(fileName).string, text: presentationData.strings.Notifications_UploadError_TooLong_Text(stringForDuration(Int32(settings.maxDuration))).string))
controllerInteraction.displayUndo(.info(title: presentationData.strings.Notifications_UploadError_TooLong_Title(fileName).string, text: presentationData.strings.Notifications_UploadError_TooLong_Text(stringForDuration(Int32(settings.maxDuration))).string, timeout: nil))
} else {
let _ = (context.engine.peers.saveNotificationSound(file: .message(message: MessageReference(message), media: file))
|> deliverOnMainQueue).start(completed: {

View File

@ -321,6 +321,8 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
case "telegram_botapp":
title = item.presentationData.strings.Conversation_BotApp
actionTitle = item.presentationData.strings.Conversation_OpenBotApp
case "telegram_community":
actionTitle = item.presentationData.strings.Conversation_OpenChatFolder
default:
break
}

View File

@ -164,7 +164,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
strongSelf.presentController(StickerPackScreen(context: strongSelf.context, mainStickerPack: new, stickerPacks: [new], parentNavigationController: strongSelf.getNavigationController()), .window(.root), nil)
return true
}
case let .editExportedInvitation(_, invite), let .revokeExportedInvitation(invite), let .deleteExportedInvitation(invite), let .participantJoinedViaInvite(invite), let .participantJoinByRequest(invite, _):
case let .editExportedInvitation(_, invite), let .revokeExportedInvitation(invite), let .deleteExportedInvitation(invite), let .participantJoinedViaInvite(invite, _), let .participantJoinByRequest(invite, _):
if let inviteLink = invite.link, !inviteLink.hasSuffix("...") {
if invite.isPermanent {
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)

View File

@ -1418,7 +1418,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil)
return ChatMessageItem(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil))
case let .participantJoinedViaInvite(invite):
case let .participantJoinedViaInvite(invite, joinedViaFolderLink):
var peers = SimpleDictionary<PeerId, Peer>()
var author: Peer?
if let peer = self.entry.peers[self.entry.event.peerId] {
@ -1429,7 +1429,12 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
var text: String = ""
var entities: [MessageTextEntity] = []
let rawText: PresentationStrings.FormattedString = self.presentationData.strings.Channel_AdminLog_JoinedViaInviteLink(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", invite.link?.replacingOccurrences(of: "https://", with: "") ?? "")
let rawText: PresentationStrings.FormattedString
if joinedViaFolderLink {
rawText = self.presentationData.strings.Channel_AdminLog_JoinedViaFolderInviteLink(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", invite.link?.replacingOccurrences(of: "https://", with: "") ?? "")
} else {
rawText = self.presentationData.strings.Channel_AdminLog_JoinedViaInviteLink(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? "", invite.link?.replacingOccurrences(of: "https://", with: "") ?? "")
}
appendAttributedText(text: rawText, generateEntities: { index in
if index == 0, let author = author {

View File

@ -593,7 +593,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
}
case let .startAttach(peerId, payload, choose):
let presentError: (String) -> Void = { errorText in
present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: errorText), elevatedLayout: true, animateInAsReplacement: false, action: { _ in
present(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: errorText, timeout: nil), elevatedLayout: true, animateInAsReplacement: false, action: { _ in
return true
}), nil)
}

View File

@ -6080,7 +6080,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|> deliverOnMainQueue).start(completed: { [weak self] in
if let strongSelf = self, let peer = strongSelf.data?.peer {
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let controller = UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Conversation_DeletedFromContacts(EnginePeer(peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false })
let controller = UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Conversation_DeletedFromContacts(EnginePeer(peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false })
controller.keepOnParentDismissal = true
strongSelf.controller?.present(controller, in: .window(.root))

View File

@ -13,7 +13,6 @@ swift_library(
"//submodules/TelegramCore:TelegramCore",
"//submodules/Postbox:Postbox",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/Display:Display",
],
visibility = [
"//visibility:public",

View File

@ -3,7 +3,12 @@ import UIKit
import Postbox
import TelegramCore
import SwiftSignalKit
import Display
private extension UIColor {
convenience init(rgb: UInt32) {
self.init(red: CGFloat((rgb >> 16) & 0xff) / 255.0, green: CGFloat((rgb >> 8) & 0xff) / 255.0, blue: CGFloat(rgb & 0xff) / 255.0, alpha: 1.0)
}
}
public enum PresentationBuiltinThemeReference: Int32 {
case dayClassic = 0

View File

@ -12,7 +12,7 @@ public enum UndoOverlayContent {
case hidArchive(title: String, text: String, undo: Bool)
case revealedArchive(title: String, text: String, undo: Bool)
case succeed(text: String)
case info(title: String?, text: String)
case info(title: String?, text: String, timeout: Double?)
case emoji(name: String, text: String)
case swipeToReply(title: String, text: String)
case actionSucceeded(title: String, text: String, cancel: String)

View File

@ -173,7 +173,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
self.textNode.maximumNumberOfLines = 5
displayUndo = false
self.originalRemainingSeconds = 3
case let .info(title, text):
case let .info(title, text, timeout):
self.avatarNode = nil
self.iconNode = nil
self.iconCheckNode = nil
@ -193,11 +193,16 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode {
self.textNode.attributedText = attributedText
self.textNode.maximumNumberOfLines = 10
displayUndo = false
self.originalRemainingSeconds = Double(max(5, min(8, text.count / 14)))
if let timeout {
self.originalRemainingSeconds = timeout
} else {
self.originalRemainingSeconds = Double(max(5, min(8, text.count / 14)))
}
if text.contains("](") {
isUserInteractionEnabled = true
}
case let .actionSucceeded(title, text, cancel):
self.avatarNode = nil
self.iconNode = nil

View File

@ -1,5 +1,5 @@
{
"app": "9.5.4",
"bazel": "5.3.1",
"bazel": "6.1.1",
"xcode": "14.2"
}