mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 13:35:19 +00:00
Merge commit '0005eebcd9cab0a3ee8ae7caa97218a094a66555' into beta
This commit is contained in:
commit
8be2f274ee
89
README.md
89
README.md
@ -11,86 +11,85 @@ There are several things we require from **all developers** for the moment.
|
||||
3. Please study our [**security guidelines**](https://core.telegram.org/mtproto/security_guidelines) and take good care of your users' data and privacy.
|
||||
4. Please remember to publish **your** code too in order to comply with the licences.
|
||||
|
||||
# Compilation Guide
|
||||
# Quick Compilation Guide
|
||||
|
||||
1. Install Xcode (directly from https://developer.apple.com/download/more or using the App Store).
|
||||
2. Clone the project from GitHub:
|
||||
## Get the Code
|
||||
|
||||
```
|
||||
git clone --recursive -j8 https://github.com/TelegramMessenger/Telegram-iOS.git
|
||||
```
|
||||
|
||||
3. Adjust configuration parameters
|
||||
## Setup Xcode
|
||||
|
||||
Install Xcode (directly from https://developer.apple.com/download/applications or using the App Store).
|
||||
|
||||
## Adjust Configuration
|
||||
|
||||
1. Generate a random identifier:
|
||||
```
|
||||
openssl rand -hex 8
|
||||
```
|
||||
2. Create a new Xcode project. Use `Telegram` as the Product Name. Use `org.{identifier from step 1}` as the Organization Identifier.
|
||||
3. Open `Keychain Access` and navigate to `Certificates`. Locate `Apple Development: your@email.address (XXXXXXXXXX)` and double tap the certificate. Under `Details`, locate `Organizational Unit`. This is the Team ID.
|
||||
4. Edit `build-system/template_minimal_development_configuration.json`. Use data from the previous steps.
|
||||
|
||||
## Generate an Xcode project
|
||||
|
||||
```
|
||||
mkdir -p $HOME/telegram-configuration
|
||||
mkdir -p $HOME/telegram-provisioning
|
||||
cp build-system/appstore-configuration.json $HOME/telegram-configuration/configuration.json
|
||||
cp -R build-system/fake-codesigning $HOME/telegram-provisioning/
|
||||
python3 build-system/Make/Make.py \
|
||||
--cacheDir="$HOME/telegram-bazel-cache" \
|
||||
generateProject \
|
||||
--configurationPath=build-system/template_minimal_development_configuration.json \
|
||||
--xcodeManagedCodesigning
|
||||
```
|
||||
|
||||
- Modify the values in `configuration.json`
|
||||
- Replace the provisioning profiles in `profiles` with valid files
|
||||
# Advanced Compilation Guide
|
||||
|
||||
4. (Optional) Create a build cache directory to speed up rebuilds
|
||||
## Xcode
|
||||
|
||||
1. Copy and edit `build-system/appstore-configuration.json`.
|
||||
2. Copy `build-system/fake-codesigning`. Create and download provisioning profiles, using the `profiles` folder as a reference for the entitlements.
|
||||
3. Generate an Xcode project:
|
||||
```
|
||||
mkdir -p "$HOME/telegram-bazel-cache"
|
||||
python3 build-system/Make/Make.py \
|
||||
--cacheDir="$HOME/telegram-bazel-cache" \
|
||||
generateProject \
|
||||
--configurationPath=configuration_from_step_1.json \
|
||||
--codesigningInformationPath=directory_from_step_2
|
||||
```
|
||||
|
||||
5. Build the app
|
||||
## IPA
|
||||
|
||||
1. Repeat the steps from the previous section. Use distribution provisioning profiles.
|
||||
2. Run:
|
||||
```
|
||||
python3 build-system/Make/Make.py \
|
||||
--cacheDir="$HOME/telegram-bazel-cache" \
|
||||
build \
|
||||
--configurationPath=path-to-configuration.json \
|
||||
--codesigningInformationPath=path-to-provisioning-data \
|
||||
--configurationPath=...see previous section... \
|
||||
--codesigningInformationPath=...see previous section... \
|
||||
--buildNumber=100001 \
|
||||
--configuration=release_universal
|
||||
--configuration=release_arm64
|
||||
```
|
||||
|
||||
6. (Optional) Generate an Xcode project
|
||||
## Tips
|
||||
|
||||
## Codesigning is not required for simulator-only builds
|
||||
|
||||
Add `--disableProvisioningProfiles`:
|
||||
```
|
||||
python3 build-system/Make/Make.py \
|
||||
--cacheDir="$HOME/telegram-bazel-cache" \
|
||||
generateProject \
|
||||
--configurationPath=path-to-configuration.json \
|
||||
--codesigningInformationPath=path-to-provisioning-data \
|
||||
--disableExtensions
|
||||
```
|
||||
|
||||
It is possible to generate a project that does not require any codesigning certificates to be installed: add `--disableProvisioningProfiles` flag:
|
||||
```
|
||||
python3 build-system/Make/Make.py \
|
||||
--cacheDir="$HOME/telegram-bazel-cache" \
|
||||
generateProject \
|
||||
--configurationPath=path-to-configuration.json \
|
||||
--codesigningInformationPath=path-to-provisioning-data \
|
||||
--disableExtensions \
|
||||
--disableProvisioningProfiles
|
||||
```
|
||||
|
||||
## Versions
|
||||
|
||||
Tip: use `--disableExtensions` when developing to speed up development by not building application extensions and the WatchOS app.
|
||||
|
||||
|
||||
# Tips
|
||||
|
||||
Bazel is used to build the app. To simplify the development setup a helper script is provided (`build-system/Make/Make.py`). See help:
|
||||
Each release is built using a specific Xcode version (see `versions.json`). The helper script checks the versions of the installed software and reports an error if they don't match the ones specified in `versions.json`. It is possible to bypass these checks:
|
||||
|
||||
```
|
||||
python3 build-system/Make/Make.py --help
|
||||
python3 build-system/Make/Make.py build --help
|
||||
python3 build-system/Make/Make.py generateProject --help
|
||||
```
|
||||
|
||||
Bazel is automatically downloaded when running Make.py for the first time. If you wish to use your own build of Bazel, pass `--bazel=path-to-bazel`. If your Bazel version differs from that in `versions.json`, you may use `--overrideBazelVersion` to skip the version check.
|
||||
|
||||
Each release is built using specific Xcode and Bazel versions (see `versions.json`). The helper script checks the versions of installed software and reports an error if they don't match the ones specified in `versions.json`. There are flags that allow to bypass these checks:
|
||||
|
||||
```
|
||||
python3 build-system/Make/Make.py --overrideBazelVersion build ... # Don't check the version of Bazel
|
||||
python3 build-system/Make/Make.py --overrideXcodeVersion build ... # Don't check the version of Xcode
|
||||
```
|
||||
|
@ -28,9 +28,13 @@ load("@build_bazel_rules_swift//swift:swift.bzl",
|
||||
load(
|
||||
"@rules_xcodeproj//xcodeproj:defs.bzl",
|
||||
"top_level_target",
|
||||
"top_level_targets",
|
||||
"xcodeproj",
|
||||
"xcode_provisioning_profile",
|
||||
)
|
||||
|
||||
load("@build_bazel_rules_apple//apple:apple.bzl", "local_provisioning_profile")
|
||||
|
||||
load("//build-system/bazel-utils:plist_fragment.bzl",
|
||||
"plist_fragment",
|
||||
)
|
||||
@ -38,6 +42,7 @@ load("//build-system/bazel-utils:plist_fragment.bzl",
|
||||
load(
|
||||
"@build_configuration//:variables.bzl",
|
||||
"telegram_bazel_path",
|
||||
"telegram_use_xcode_managed_codesigning",
|
||||
"telegram_bundle_id",
|
||||
"telegram_aps_environment",
|
||||
"telegram_team_id",
|
||||
@ -509,10 +514,11 @@ app_groups_fragment = """
|
||||
telegram_bundle_id=telegram_bundle_id
|
||||
)
|
||||
|
||||
communication_notifications_fragment = """
|
||||
official_communication_notifications_fragment = """
|
||||
<key>com.apple.developer.usernotifications.communication</key>
|
||||
<true/>
|
||||
"""
|
||||
communication_notifications_fragment = official_communication_notifications_fragment if telegram_bundle_id in official_bundle_ids else ""
|
||||
|
||||
store_signin_fragment = """
|
||||
<key>com.apple.developer.applesignin</key>
|
||||
@ -1919,6 +1925,18 @@ plist_fragment(
|
||||
)
|
||||
)
|
||||
|
||||
local_provisioning_profile(
|
||||
name = "Telegram_local_profile",
|
||||
profile_name = "iOS Team Provisioning Profile: {}".format(telegram_bundle_id),
|
||||
team_id = telegram_team_id,
|
||||
)
|
||||
|
||||
xcode_provisioning_profile(
|
||||
name = "Telegram_xcode_profile",
|
||||
managed_by_xcode = True,
|
||||
provisioning_profile = ":Telegram_local_profile",
|
||||
)
|
||||
|
||||
ios_application(
|
||||
name = "Telegram",
|
||||
bundle_id = "{telegram_bundle_id}".format(
|
||||
@ -1928,7 +1946,7 @@ ios_application(
|
||||
minimum_os_version = minimum_os_version,
|
||||
provisioning_profile = select({
|
||||
":disableProvisioningProfilesSetting": None,
|
||||
"//conditions:default": "@build_configuration//provisioning:Telegram.mobileprovision",
|
||||
"//conditions:default": ":Telegram_xcode_profile" if telegram_use_xcode_managed_codesigning else "@build_configuration//provisioning:Telegram.mobileprovision",
|
||||
}),
|
||||
entitlements = ":TelegramEntitlements.entitlements",
|
||||
infoplists = [
|
||||
@ -1985,9 +2003,22 @@ xcodeproj(
|
||||
bazel_path = telegram_bazel_path,
|
||||
project_name = "Telegram",
|
||||
tags = ["manual"],
|
||||
top_level_targets = [
|
||||
":Telegram",
|
||||
],
|
||||
top_level_targets = top_level_targets(
|
||||
labels = [
|
||||
":Telegram",
|
||||
],
|
||||
target_environments = ["device", "simulator"],
|
||||
),
|
||||
xcode_configurations = {
|
||||
"Debug": {
|
||||
"//command_line_option:compilation_mode": "dbg",
|
||||
},
|
||||
"Release": {
|
||||
"//command_line_option:compilation_mode": "opt",
|
||||
},
|
||||
},
|
||||
default_xcode_configuration = "Debug"
|
||||
|
||||
)
|
||||
|
||||
# Temporary targets used to simplify webrtc build tests
|
||||
|
@ -21,6 +21,7 @@ swift_library(
|
||||
"//submodules/rlottie:RLottieBinding",
|
||||
"//submodules/GZip:GZip",
|
||||
"//submodules/PersistentStringHash:PersistentStringHash",
|
||||
"//submodules/Utils/RangeSet",
|
||||
|
||||
],
|
||||
visibility = [
|
||||
|
@ -15,6 +15,7 @@ import PersistentStringHash
|
||||
import CallKit
|
||||
import AppLockState
|
||||
import NotificationsPresentationData
|
||||
import RangeSet
|
||||
|
||||
private let queue = Queue()
|
||||
|
||||
@ -680,7 +681,7 @@ private final class NotificationServiceHandler {
|
||||
Logger.shared.logToConsole = loggingSettings.logToConsole
|
||||
Logger.shared.redactSensitiveData = loggingSettings.redactSensitiveData
|
||||
|
||||
let networkArguments = NetworkInitializationArguments(apiId: apiId, apiHash: apiHash, languagesCategory: languagesCategory, appVersion: appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider(), deviceModelName: nil, useBetaFeatures: !buildConfig.isAppStoreBuild)
|
||||
let networkArguments = NetworkInitializationArguments(apiId: apiId, apiHash: apiHash, languagesCategory: languagesCategory, appVersion: appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider(), deviceModelName: nil, useBetaFeatures: !buildConfig.isAppStoreBuild, isICloudEnabled: false)
|
||||
|
||||
let isLockedMessage: String?
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: appLockStatePath(rootPath: rootPath))), let state = try? JSONDecoder().decode(LockState.self, from: data), isAppLocked(state: state) {
|
||||
@ -1187,7 +1188,7 @@ private final class NotificationServiceHandler {
|
||||
fetchMediaSignal = Signal { subscriber in
|
||||
final class DataValue {
|
||||
var data = Data()
|
||||
var totalSize: Int64?
|
||||
var missingRanges = RangeSet<Int64>(0 ..< Int64.max)
|
||||
}
|
||||
|
||||
let collectedData = Atomic<DataValue>(value: DataValue())
|
||||
@ -1217,12 +1218,22 @@ private final class NotificationServiceHandler {
|
||||
useMainConnection: true
|
||||
).start(next: { result in
|
||||
switch result {
|
||||
case let .dataPart(_, data, _, _):
|
||||
case let .dataPart(offset, data, dataRange, _):
|
||||
var isCompleted = false
|
||||
let _ = collectedData.modify { current in
|
||||
let current = current
|
||||
current.data.append(data)
|
||||
if let totalSize = current.totalSize, Int64(current.data.count) >= totalSize {
|
||||
|
||||
let fillRange = Int(offset) ..< (Int(offset) + data.count)
|
||||
if current.data.count < fillRange.upperBound {
|
||||
current.data.count = fillRange.upperBound
|
||||
}
|
||||
current.data.withUnsafeMutableBytes { buffer -> Void in
|
||||
let bytes = buffer.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||
data.copyBytes(to: bytes.advanced(by: Int(offset)), from: Int(dataRange.lowerBound) ..< Int(dataRange.upperBound))
|
||||
}
|
||||
current.missingRanges.remove(contentsOf: Int64(fillRange.lowerBound) ..< Int64(fillRange.upperBound))
|
||||
|
||||
if current.missingRanges.isEmpty {
|
||||
isCompleted = true
|
||||
}
|
||||
return current
|
||||
@ -1235,8 +1246,8 @@ private final class NotificationServiceHandler {
|
||||
var isCompleted = false
|
||||
let _ = collectedData.modify { current in
|
||||
let current = current
|
||||
current.totalSize = size
|
||||
if Int64(current.data.count) >= size {
|
||||
current.missingRanges.remove(contentsOf: size ..< Int64.max)
|
||||
if current.missingRanges.isEmpty {
|
||||
isCompleted = true
|
||||
}
|
||||
return current
|
||||
|
@ -174,7 +174,7 @@ class DefaultIntentHandler: INExtension, INSendMessageIntentHandling, INSearchFo
|
||||
if let accountCache = accountCache {
|
||||
account = .single(accountCache)
|
||||
} else {
|
||||
account = currentAccount(allocateIfNotExists: false, networkArguments: NetworkInitializationArguments(apiId: apiId, apiHash: apiHash, languagesCategory: languagesCategory, appVersion: appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider(), deviceModelName: nil, useBetaFeatures: !buildConfig.isAppStoreBuild), supplementary: true, manager: accountManager, rootPath: rootPath, auxiliaryMethods: accountAuxiliaryMethods, encryptionParameters: encryptionParameters)
|
||||
account = currentAccount(allocateIfNotExists: false, networkArguments: NetworkInitializationArguments(apiId: apiId, apiHash: apiHash, languagesCategory: languagesCategory, appVersion: appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider(), deviceModelName: nil, useBetaFeatures: !buildConfig.isAppStoreBuild, isICloudEnabled: false), supplementary: true, manager: accountManager, rootPath: rootPath, auxiliaryMethods: accountAuxiliaryMethods, encryptionParameters: encryptionParameters)
|
||||
|> mapToSignal { account -> Signal<Account?, NoError> in
|
||||
if let account = account {
|
||||
switch account {
|
||||
|
@ -255,8 +255,8 @@
|
||||
|
||||
"PUSH_MESSAGE_SUGGEST_USERPIC" = "%1$@|suggested you new profile photo";
|
||||
|
||||
"PUSH_MESSAGE_WALLPAPER" = "%1$@ set a new background for the chat with you";
|
||||
"PUSH_MESSAGE_SAME_WALLPAPER" = "%1$@ set the same background for the chat with you";
|
||||
"PUSH_MESSAGE_WALLPAPER" = "%1$@ set a new wallpaper for the chat with you";
|
||||
"PUSH_MESSAGE_SAME_WALLPAPER" = "%1$@ set the same wallpaper for the chat with you";
|
||||
|
||||
"PUSH_REMINDER_TITLE" = "🗓 Reminder";
|
||||
"PUSH_SENDER_YOU" = "📅 You";
|
||||
@ -9121,12 +9121,13 @@ Sorry for the inconvenience.";
|
||||
|
||||
"Premium.GiftedTitle.Someone" = "Someone";
|
||||
|
||||
"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";
|
||||
"Notification.ChangedWallpaper" = "%1$@ set a new wallpaper for this chat";
|
||||
"Notification.YouChangedWallpaper" = "You set a new wallpaper for this chat";
|
||||
"Notification.YouChangingWallpaper" = "Setting new wallpaper";
|
||||
"Notification.Wallpaper.View" = "View Wallpaper";
|
||||
|
||||
"Notification.ChangedToSameWallpaper" = "%1$@ set the same background for this chat";
|
||||
"Notification.YouChangedToSameWallpaper" = "You set the same background for this chat";
|
||||
"Notification.ChangedToSameWallpaper" = "%1$@ set the same wallpaper for this chat";
|
||||
"Notification.YouChangedToSameWallpaper" = "You set the same wallpaper for this chat";
|
||||
|
||||
"Channel.AdminLog.JoinedViaFolderInviteLink" = "%1$@ joined via invite link %2$@ (community)";
|
||||
|
||||
@ -9144,10 +9145,185 @@ Sorry for the inconvenience.";
|
||||
"PeerInfo.Bot.ChangeSettings" = "Change Bot Settings";
|
||||
"PeerInfo.Bot.BotFatherInfo" = "Use [@BotFather]() to manage this bot.";
|
||||
|
||||
"WallpaperPreview.NotAppliedInfo" = "Background will not be applied for **%@**";
|
||||
"WallpaperPreview.ChatTopText" = "Apply the background in this chat.";
|
||||
"WallpaperPreview.NotAppliedInfo" = "**%@** will be able to apply this wallpaper";
|
||||
"WallpaperPreview.ChatTopText" = "Apply the wallpaper in this chat.";
|
||||
"WallpaperPreview.ChatBottomText" = "Enjoy the view.";
|
||||
|
||||
"Conversation.Theme.SetPhotoWallpaper" = "Choose Background from Photos";
|
||||
"Conversation.Theme.SetColorWallpaper" = "Choose Color as a Background";
|
||||
"Conversation.Theme.OtherOptions" = "Other Options...";
|
||||
"Conversation.Theme.SetPhotoWallpaper" = "Choose Wallpaper from Photos";
|
||||
"Conversation.Theme.SetColorWallpaper" = "Set a Color as Wallpaper";
|
||||
|
||||
"Conversation.Theme.ChooseWallpaperTitle" = "Choose Wallpaper";
|
||||
"Conversation.Theme.ResetWallpaper" = "Remove Wallpaper";
|
||||
"Conversation.Theme.ChooseColorTitle" = "Set a Color";
|
||||
"Conversation.Theme.SetCustomColor" = "Set Custom";
|
||||
|
||||
"Appearance.ShowNextMediaOnTap" = "Show Next Media on Tap";
|
||||
"Appearance.ShowNextMediaOnTapInfo" = "Tap near the edge of the screen while viewing media to navigate between photos.";
|
||||
|
||||
"WebApp.LaunchMoreInfo" = "More about this bot";
|
||||
"WebApp.LaunchConfirmation" = "To launch this web app, you will connect to its website.";
|
||||
|
||||
"WallpaperPreview.PreviewInNightMode" = "Preview this wallpaper in night mode.";
|
||||
"WallpaperPreview.PreviewInDayMode" = "Preview this wallpaper in day mode.";
|
||||
|
||||
"PeerInfo.BotBlockedTitle" = "Bot Blocked";
|
||||
"PeerInfo.BotBlockedText" = "This bot will not be able to message you.";
|
||||
|
||||
"ChatList.ContextMuteAll" = "Mute All";
|
||||
"ChatList.ContextUnmuteAll" = "Unmute All";
|
||||
"ChatList.ToastFolderMuted" = "All chats in **%@** are now muted";
|
||||
"ChatList.ToastFolderUnmuted" = "All chats in **%@** are now unmuted";
|
||||
"ChatList.ContextMenuShare" = "Share";
|
||||
"ChatList.ContextMenuBadgeNew" = "NEW";
|
||||
"ChatList.AlertDeleteFolderTitle" = "Delete Folder";
|
||||
"ChatList.AlertDeleteFolderText" = "Are you sure you want to delete this folder? This will also deactivate all the invite links used to share this folder.";
|
||||
"ChatList.PanelNewChatsAvailable_1" = "1 New Chat Available";
|
||||
"ChatList.PanelNewChatsAvailable_any" = "%d New Chats Available";
|
||||
|
||||
"ChatListFilter.SectionShare" = "SHARE FOLDER";
|
||||
"ChatListFilter.CreateLink" = "Create a new Link";
|
||||
"ChatListFilter.CreateLinkNew" = "Create an Invite Link";
|
||||
"ChatListFilter.ExcludeChatsAction" = "Add Chats to Exclude";
|
||||
"ChatListFilter.LinkListInfo" = "Create more links to set up different access levels for different people.";
|
||||
"ChatListFilter.LinkListInfoNew" = "Share access to some of this folder's groups and channels with others.";
|
||||
"ChatListFilter.ToastChatsAddedTitle_1" = "Сhat added to folder";
|
||||
"ChatListFilter.ToastChatsAddedTitle_any" = "%d chats added to folder";
|
||||
"ChatListFilter.ToastChatsAddedText" = "It will not affect chatlist of the links of this folder";
|
||||
"ChatListFilter.ToastChatsRemovedTitle_1" = "Сhat removed from folder";
|
||||
"ChatListFilter.ToastChatsRemovedTitle_any" = "%d chats added to folder";
|
||||
"ChatListFilter.ToastChatsRemovedText" = "It will not affect chatlist of the links of this folder";
|
||||
"ChatListFilter.AlertCreateFolderBeforeSharingText" = "Please finish creating this folder to share it.";
|
||||
"ChatListFilter.ErrorShareInvalidFolder" = "You can’t share folders which have chat types or excluded chats.";
|
||||
"ChatListFilter.SaveAlertActionSave" = "Save";
|
||||
"ChatListFilter.CreateLinkUnknownError" = "Error creating a share link";
|
||||
"ChatListFilter.CreateLinkErrorSomeoneHasChannelLimit" = "One of the groups in this folder can’t be added because one of its admins has too many groups and channels.";
|
||||
|
||||
"ChatListFilterList.CreateFolder" = "Create a Folder";
|
||||
|
||||
"FolderLinkScreen.LabelCanInvite" = "you can invite others here";
|
||||
|
||||
"FolderLinkScreen.LabelUnavailableBot" = "you can't share chats with bots";
|
||||
"FolderLinkScreen.AlertTextUnavailableBot" = "You can't share chats with bots";
|
||||
|
||||
"FolderLinkScreen.LabelUnavailableUser" = "you can't share private chats";
|
||||
"FolderLinkScreen.AlertTextUnavailableUser" = "You can't share private chats";
|
||||
|
||||
"FolderLinkScreen.LabelUnavailableGeneric" = "you can't invite others here";
|
||||
"FolderLinkScreen.AlertTextUnavailablePrivateGroup" = "You don't have the admin rights to share invite links to this private group.";
|
||||
"FolderLinkScreen.AlertTextUnavailablePublicGroup" = "You don't have the admin rights to share invite links to this group chat.";
|
||||
"FolderLinkScreen.AlertTextUnavailablePrivateChannel" = "You don't have the admin rights to share invite links to this private channel.";
|
||||
"FolderLinkScreen.AlertTextUnavailablePublicChannel" = "You don't have the admin rights to share invite links to this channel.";
|
||||
|
||||
"FolderLinkScreen.TitleDescriptionUnavailable" = "You can only share groups and channels in which you are allowed to create invite links.";
|
||||
"FolderLinkScreen.ChatCountHeaderUnavailable" = "There are no chats in this folder that you can share with others.";
|
||||
"FolderLinkScreen.ChatsSectionHeaderUnavailable" = "THESE CHATS CANNOT BE SHARED";
|
||||
|
||||
"FolderLinkScreen.TitleDescriptionDeselected" = "Anyone with this link can add **%@** folder and the chats selected below.";
|
||||
"FolderLinkScreen.ChatsSectionHeader" = "CHATS";
|
||||
"FolderLinkScreen.ChatsSectionHeaderActionSelectAll" = "SELECT ALL";
|
||||
"FolderLinkScreen.ChatsSectionHeaderActionDeselectAll" = "DESELECT ALL";
|
||||
|
||||
"FolderLinkScreen.TitleDescriptionSelectedCount_1" = "the 1 chat";
|
||||
"FolderLinkScreen.TitleDescriptionSelectedCount_any" = "the %d chats";
|
||||
"FolderLinkScreen.TitleDescriptionSelected" = "Anyone with this link can add **%1$@** folder and %2$@ selected below.";
|
||||
|
||||
"FolderLinkScreen.ChatsSectionHeaderSelected_1" = "1 CHAT SELECTED";
|
||||
"FolderLinkScreen.ChatsSectionHeaderSelected_any" = "%d CHATS SELECTED";
|
||||
|
||||
"FolderLinkScreen.LinkSectionHeader" = "INVITE LINK";
|
||||
|
||||
"FolderLinkScreen.ContextActionNameLink" = "Name Link";
|
||||
"FolderLinkScreen.NameLink.Title" = "Name This Link";
|
||||
|
||||
"FolderLinkScreen.ToastNewChatAdded" = "People who already used the invite link will be able to join newly added chats.";
|
||||
"FolderLinkScreen.SaveUnknownError" = "An error occurred while updating the link";
|
||||
"FolderLinkScreen.ToastLinkUpdated" = "Link updated";
|
||||
|
||||
"FolderLinkScreen.Title" = "Share Folder";
|
||||
|
||||
"FolderLinkScreen.SaveAlertTitle" = "Unsaved Changes";
|
||||
"FolderLinkScreen.SaveAlertText" = "You have changed the settings of this folder. Apply changes?";
|
||||
"FolderLinkScreen.SaveAlertActionDiscard" = "Discard";
|
||||
"FolderLinkScreen.SaveAlertActionApply" = "Apply";
|
||||
"FolderLinkScreen.SaveAlertActionContinue" = "Cancel";
|
||||
|
||||
"FolderLinkScreen.LinkActionCopy" = "Copy";
|
||||
"FolderLinkScreen.LinkActionShare" = "Share";
|
||||
|
||||
"InviteLink.LabelJoinedViaFolder" = "joined via a folder invite link";
|
||||
|
||||
"ChatListFilter.LinkLabelChatCount_1" = "includes 1 chat";
|
||||
"ChatListFilter.LinkLabelChatCount_any" = "includes %d chats";
|
||||
"ChatListFilter.LinkActionDelete" = "Delete";
|
||||
|
||||
"InviteLink.QRCodeFolder.Title" = "Invite by QR Code";
|
||||
"InviteLink.QRCodeFolder.Text" = "Everyone on Telegram can scan this code to add this folder and join the chats included in this invite link.";
|
||||
|
||||
"FolderLinkPreview.IconTabLeft" = "All Chats";
|
||||
"FolderLinkPreview.IconTabRight" = "Personal";
|
||||
"FolderLinkPreview.TitleShare" = "Share Folder";
|
||||
"FolderLinkPreview.TitleRemove" = "Remove Folder";
|
||||
"FolderLinkPreview.TitleAddFolder" = "Add Folder";
|
||||
"FolderLinkPreview.TitleAddChats_1" = "Add %d Chat";
|
||||
"FolderLinkPreview.TitleAddChats_any" = "Add %d Chats";
|
||||
"FolderLinkPreview.LinkSectionHeader" = "INVITE LINKS";
|
||||
"FolderLinkPreview.RemoveSectionSelectedHeader_1" = "%d CHAT TO QUIT";
|
||||
"FolderLinkPreview.RemoveSectionSelectedHeader_any" = "%d CHATS TO QUIT";
|
||||
"FolderLinkPreview.ChatSectionHeader_1" = "1 CHAT IN THIS FOLDER";
|
||||
"FolderLinkPreview.ChatSectionHeader_any" = "%d CHATS IN THIS FOLDER";
|
||||
"FolderLinkPreview.ChatSectionJoinHeader_1" = "1 CHAT IN THIS FOLDER TO JOIN";
|
||||
"FolderLinkPreview.ChatSectionJoinHeader_any" = "%d CHATS IN THIS FOLDER TO JOIN";
|
||||
|
||||
"FolderLinkPreview.ToastChatsAddedTitle" = "Folder %@ Updated";
|
||||
"FolderLinkPreview.ToastChatsAddedText_1" = "You have joined %d new chat";
|
||||
"FolderLinkPreview.ToastChatsAddedText_any" = "You have joined %d new chats";
|
||||
|
||||
"FolderLinkPreview.ToastFolderAddedTitle" = "Folder %@ Added";
|
||||
"FolderLinkPreview.ToastFolderAddedText_1" = "You also joined %d chat";
|
||||
"FolderLinkPreview.ToastFolderAddedText_any" = "You also joined %d chats";
|
||||
|
||||
"FolderLinkPreview.TextLinkList" = "Create more links to set up different access\nlevels for different people.";
|
||||
"FolderLinkPreview.TextRemoveFolder" = "Do you also want to quit the chats included in this folder?";
|
||||
"FolderLinkPreview.TextAllAdded" = "You have already added this\nfolder and its chats.";
|
||||
"FolderLinkPreview.TextAddFolder" = "Do you want to add a new chat folder\nand join its groups and channels?";
|
||||
|
||||
"FolderLinkPreview.TextAddChatsCount_1" = "%d chat";
|
||||
"FolderLinkPreview.TextAddChatsCount_any" = "%d chats";
|
||||
"FolderLinkPreview.TextAddChats" = "Do you want to add **%1$@** to the\nfolder **%2$@**?";
|
||||
|
||||
"FolderLinkPreview.LabelPeerSubscriber" = "You are already a subscriber";
|
||||
"FolderLinkPreview.LabelPeerSubscribers_1" = "%d subscriber";
|
||||
"FolderLinkPreview.LabelPeerSubscribers_any" = "%d subscribers";
|
||||
|
||||
"FolderLinkPreview.LabelPeerMember" = "You are already a member";
|
||||
"FolderLinkPreview.LabelPeerMembers_1" = "%d member";
|
||||
"FolderLinkPreview.LabelPeerMembers_any" = "%d members";
|
||||
|
||||
"FolderLinkPreview.ToastAlreadyMemberChannel" = "You are already subscribed to this channel.";
|
||||
"FolderLinkPreview.ToastAlreadyMemberGroup" = "You are already a member of this group.";
|
||||
|
||||
"FolderLinkPreview.ButtonRemoveFolder" = "Remove Folder";
|
||||
"FolderLinkPreview.ButtonRemoveFolderAndChats" = "Remove Folder and Chats";
|
||||
|
||||
"FolderLinkPreview.ButtonDoNotJoinChats" = "Do Not Join Any Chats";
|
||||
"FolderLinkPreview.ButtonJoinChats" = "Join Chats";
|
||||
"FolderLinkPreview.ButtonAddFolder" = "Add Folder";
|
||||
|
||||
"FolderLinkPreview.ToastLeftTitle" = "Folder %@ deleted";
|
||||
"FolderLinkPreview.ToastLeftChatsText_1" = "You also left **%d** chat";
|
||||
"FolderLinkPreview.ToastLeftChatsText_any" = "You also left **%d** chats";
|
||||
|
||||
"FolderLinkPreview.ListSelectionSelectAllDynamicPartSelect" = "SELECT";
|
||||
"FolderLinkPreview.ListSelectionSelectAllDynamicPartDeselect" = "DESELECT";
|
||||
"FolderLinkPreview.ListSelectionSelectAllStaticPartSelect" = "ALL";
|
||||
"FolderLinkPreview.ListSelectionSelectAllStaticPartDeselect" = "ALL";
|
||||
"FolderLinkPreview.ListSelectionSelectAllFormat" = "{dynamic}{static}";
|
||||
|
||||
"UserInfo.ChangeWallpaper" = "Change Wallpaper";
|
||||
|
||||
"Conversation.Theme.PreviewDarkShort" = "Tap to view this theme in the night mode.";
|
||||
"Conversation.Theme.PreviewLightShort" = "Tap to view this theme in the day mode.";
|
||||
|
||||
"ChatList.EmptyListContactsHeader" = "YOUR CONTACTS ON TELEGRAM";
|
||||
"ChatList.EmptyListContactsHeaderHide" = "hide";
|
||||
"ChatList.EmptyListTooltip" = "Send a message or\nstart a group here.";
|
||||
|
@ -75,3 +75,9 @@ http_archive(
|
||||
sha256 = "032907801dc7784744a1ca8fd40d3eecc34a2e27a93a4b3993f617cca204a9f3",
|
||||
build_file = "@//third-party/AppCenter:AppCenter.BUILD",
|
||||
)
|
||||
|
||||
load("@build_bazel_rules_apple//apple:apple.bzl", "provisioning_profile_repository")
|
||||
|
||||
provisioning_profile_repository(
|
||||
name = "local_provisioning_profiles",
|
||||
)
|
||||
|
@ -35,9 +35,10 @@ class BuildConfiguration:
|
||||
self.enable_siri = enable_siri
|
||||
self.enable_icloud = enable_icloud
|
||||
|
||||
def write_to_variables_file(self, bazel_path, aps_environment, path):
|
||||
def write_to_variables_file(self, bazel_path, use_xcode_managed_codesigning, aps_environment, path):
|
||||
string = ''
|
||||
string += 'telegram_bazel_path = "{}"\n'.format(bazel_path)
|
||||
string += 'telegram_use_xcode_managed_codesigning = {}\n'.format('True' if use_xcode_managed_codesigning else 'False')
|
||||
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)
|
||||
@ -240,6 +241,9 @@ class CodesigningSource:
|
||||
raise Exception('Not implemented')
|
||||
|
||||
def resolve_aps_environment(self):
|
||||
raise Exception('Not implemented')
|
||||
|
||||
def use_xcode_managed_codesigning(self):
|
||||
raise Exception('Not implemented')
|
||||
|
||||
def copy_certificates_to_destination(self, destination_path):
|
||||
@ -280,6 +284,9 @@ class GitCodesigningSource(CodesigningSource):
|
||||
source_path = self.working_dir + '/decrypted/profiles/{}'.format(self.codesigning_type)
|
||||
return resolve_aps_environment_from_directory(source_path=source_path, team_id=self.team_id, bundle_id=self.bundle_id)
|
||||
|
||||
def use_xcode_managed_codesigning(self):
|
||||
return False
|
||||
|
||||
def copy_certificates_to_destination(self, destination_path):
|
||||
source_path = None
|
||||
if self.codesigning_type in ['adhoc', 'appstore']:
|
||||
@ -308,5 +315,28 @@ class DirectoryCodesigningSource(CodesigningSource):
|
||||
def resolve_aps_environment(self):
|
||||
return resolve_aps_environment_from_directory(source_path=self.directory_path + '/profiles', team_id=self.team_id, bundle_id=self.bundle_id)
|
||||
|
||||
def use_xcode_managed_codesigning(self):
|
||||
return False
|
||||
|
||||
def copy_certificates_to_destination(self, destination_path):
|
||||
copy_certificates_from_directory(source_path=self.directory_path + '/certs', destination_path=destination_path)
|
||||
|
||||
|
||||
class XcodeManagedCodesigningSource(CodesigningSource):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def load_data(self, working_dir):
|
||||
pass
|
||||
|
||||
def copy_profiles_to_destination(self, destination_path):
|
||||
pass
|
||||
|
||||
def resolve_aps_environment(self):
|
||||
return ""
|
||||
|
||||
def use_xcode_managed_codesigning(self):
|
||||
return True
|
||||
|
||||
def copy_certificates_to_destination(self, destination_path):
|
||||
pass
|
||||
|
@ -12,14 +12,15 @@ import glob
|
||||
from BuildEnvironment import resolve_executable, call_executable, run_executable_with_output, BuildEnvironment
|
||||
from ProjectGeneration import generate
|
||||
from BazelLocation import locate_bazel
|
||||
from BuildConfiguration import CodesigningSource, GitCodesigningSource, DirectoryCodesigningSource, BuildConfiguration, build_configuration_from_json
|
||||
from BuildConfiguration import CodesigningSource, GitCodesigningSource, DirectoryCodesigningSource, XcodeManagedCodesigningSource, BuildConfiguration, build_configuration_from_json
|
||||
import RemoteBuild
|
||||
import GenerateProfiles
|
||||
|
||||
|
||||
class ResolvedCodesigningData:
|
||||
def __init__(self, aps_environment):
|
||||
def __init__(self, aps_environment, use_xcode_managed_codesigning):
|
||||
self.aps_environment = aps_environment
|
||||
self.use_xcode_managed_codesigning = use_xcode_managed_codesigning
|
||||
|
||||
|
||||
class BazelCommandLine:
|
||||
@ -451,8 +452,8 @@ def resolve_codesigning(arguments, base_path, build_configuration, provisioning_
|
||||
team_id=build_configuration.team_id,
|
||||
bundle_id=build_configuration.bundle_id
|
||||
)
|
||||
elif arguments.noCodesigning is not None:
|
||||
return ResolvedCodesigningData(aps_environment='production')
|
||||
elif arguments.xcodeManagedCodesigning is not None and arguments.xcodeManagedCodesigning == True:
|
||||
profile_source = XcodeManagedCodesigningSource()
|
||||
else:
|
||||
raise Exception('Neither gitCodesigningRepository nor codesigningInformationPath are set')
|
||||
|
||||
@ -467,7 +468,10 @@ def resolve_codesigning(arguments, base_path, build_configuration, provisioning_
|
||||
profile_source.copy_profiles_to_destination(destination_path=additional_codesigning_output_path + '/profiles')
|
||||
profile_source.copy_certificates_to_destination(destination_path=additional_codesigning_output_path + '/certs')
|
||||
|
||||
return ResolvedCodesigningData(aps_environment=profile_source.resolve_aps_environment())
|
||||
return ResolvedCodesigningData(
|
||||
aps_environment=profile_source.resolve_aps_environment(),
|
||||
use_xcode_managed_codesigning=profile_source.use_xcode_managed_codesigning()
|
||||
)
|
||||
|
||||
|
||||
def resolve_configuration(base_path, bazel_command_line: BazelCommandLine, arguments, additional_codesigning_output_path):
|
||||
@ -499,7 +503,7 @@ def resolve_configuration(base_path, bazel_command_line: BazelCommandLine, argum
|
||||
sys.exit(1)
|
||||
|
||||
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')
|
||||
build_configuration.write_to_variables_file(bazel_path=bazel_command_line.bazel, use_xcode_managed_codesigning=codesigning_data.use_xcode_managed_codesigning, aps_environment=codesigning_data.aps_environment, path=configuration_repository_path + '/variables.bzl')
|
||||
|
||||
provisioning_profile_files = []
|
||||
for file_name in os.listdir(provisioning_path):
|
||||
@ -549,6 +553,8 @@ def generate_project(bazel, arguments):
|
||||
disable_extensions = arguments.disableExtensions
|
||||
if arguments.disableProvisioningProfiles is not None:
|
||||
disable_provisioning_profiles = arguments.disableProvisioningProfiles
|
||||
if arguments.xcodeManagedCodesigning is not None and arguments.xcodeManagedCodesigning == True:
|
||||
disable_extensions = True
|
||||
if arguments.generateDsym is not None:
|
||||
generate_dsym = arguments.generateDsym
|
||||
if arguments.target is not None:
|
||||
@ -594,9 +600,6 @@ def build(bazel, arguments):
|
||||
bazel_command_line.set_show_actions(arguments.showActions)
|
||||
bazel_command_line.set_enable_sandbox(arguments.sandbox)
|
||||
|
||||
if arguments.noCodesigning is not None:
|
||||
bazel_command_line.set_disable_provisioning_profiles()
|
||||
|
||||
bazel_command_line.set_split_swiftmodules(arguments.enableParallelSwiftmoduleGeneration)
|
||||
|
||||
bazel_command_line.invoke_build()
|
||||
@ -688,12 +691,11 @@ def add_codesigning_common_arguments(current_parser: argparse.ArgumentParser):
|
||||
metavar='command'
|
||||
)
|
||||
codesigning_group.add_argument(
|
||||
'--noCodesigning',
|
||||
type=bool,
|
||||
'--xcodeManagedCodesigning',
|
||||
action='store_true',
|
||||
help='''
|
||||
Use signing certificates and provisioning profiles from a local directory.
|
||||
Let Xcode manage your certificates and provisioning profiles.
|
||||
''',
|
||||
metavar='command'
|
||||
)
|
||||
|
||||
current_parser.add_argument(
|
||||
|
@ -10,21 +10,29 @@ def remove_directory(path):
|
||||
shutil.rmtree(path)
|
||||
|
||||
def generate_xcodeproj(build_environment: BuildEnvironment, disable_extensions, disable_provisioning_profiles, generate_dsym, configuration_path, bazel_app_arguments, target_name):
|
||||
if '/' in target_name:
|
||||
app_target_spec = target_name.split('/')[0] + '/' + target_name.split('/')[1] + ':' + target_name.split('/')[1]
|
||||
app_target = target_name
|
||||
app_target_clean = app_target.replace('/', '_')
|
||||
else:
|
||||
app_target_spec = '{target}:{target}'.format(target=target_name)
|
||||
app_target = target_name
|
||||
app_target_clean = app_target.replace('/', '_')
|
||||
|
||||
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')]
|
||||
if disable_extensions:
|
||||
bazel_generate_arguments += ['--//{}:disableExtensions'.format(app_target)]
|
||||
bazel_generate_arguments += ['--//{}:disableStripping'.format('Telegram')]
|
||||
|
||||
project_bazel_arguments = []
|
||||
for argument in bazel_app_arguments:
|
||||
project_bazel_arguments.append(argument)
|
||||
project_bazel_arguments += ['--override_repository=build_configuration={}'.format(configuration_path)]
|
||||
if disable_extensions:
|
||||
project_bazel_arguments += ['--//{}:disableExtensions'.format(app_target)]
|
||||
project_bazel_arguments += ['--//{}:disableStripping'.format('Telegram')]
|
||||
|
||||
xcodeproj_bazelrc = os.path.join(build_environment.base_path, 'xcodeproj.bazelrc')
|
||||
if os.path.isfile(xcodeproj_bazelrc):
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit dc226d129aca2237982b98a95c80ed1ccc74f0c5
|
||||
Subproject commit 7f600ddd7cb3ebc59c696a4639b84b93267c7b6e
|
14
build-system/template_minimal_development_configuration.json
Executable file
14
build-system/template_minimal_development_configuration.json
Executable file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"bundle_id": "org.{! a random string !}.Telegram",
|
||||
"api_id": "{! get one at https://my.telegram.org/apps !}",
|
||||
"api_hash": "{! get one at https://my.telegram.org/apps !}",
|
||||
"team_id": "{! check README.md !}",
|
||||
"app_center_id": "0",
|
||||
"is_internal_build": "true",
|
||||
"is_appstore_build": "false",
|
||||
"appstore_id": "0",
|
||||
"app_specific_url_scheme": "tg",
|
||||
"premium_iap_product_id": "",
|
||||
"enable_siri": false,
|
||||
"enable_icloud": false
|
||||
}
|
@ -756,6 +756,7 @@ public protocol SharedAccountContext: AnyObject {
|
||||
var currentInAppNotificationSettings: Atomic<InAppNotificationSettings> { get }
|
||||
var currentMediaInputSettings: Atomic<MediaInputSettings> { get }
|
||||
var currentStickerSettings: Atomic<StickerSettings> { get }
|
||||
var currentMediaDisplaySettings: Atomic<MediaDisplaySettings> { get }
|
||||
|
||||
var energyUsageSettings: EnergyUsageSettings { get }
|
||||
|
||||
|
@ -330,7 +330,7 @@ private final class AnimatedStickerDirectFrameSourceCache {
|
||||
return .notFound
|
||||
}
|
||||
|
||||
self.file.seek(position: Int64(index * 4 * 2))
|
||||
let _ = self.file.seek(position: Int64(index * 4 * 2))
|
||||
var offset: Int32 = 0
|
||||
var length: Int32 = 0
|
||||
if self.file.read(&offset, 4) != 4 {
|
||||
@ -384,12 +384,12 @@ private final class AnimatedStickerDirectFrameSourceCache {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.file.seek(position: Int64(index * 4 * 2))
|
||||
let _ = strongSelf.file.seek(position: Int64(index * 4 * 2))
|
||||
var offset = Int32(currentSize)
|
||||
var length = Int32(compressedData.data.count)
|
||||
let _ = strongSelf.file.write(&offset, count: 4)
|
||||
let _ = strongSelf.file.write(&length, count: 4)
|
||||
strongSelf.file.seek(position: Int64(currentSize))
|
||||
let _ = strongSelf.file.seek(position: Int64(currentSize))
|
||||
compressedData.data.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) -> Void in
|
||||
if let baseAddress = buffer.baseAddress {
|
||||
let _ = strongSelf.file.write(baseAddress, count: Int(length))
|
||||
@ -427,12 +427,12 @@ private final class AnimatedStickerDirectFrameSourceCache {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.file.seek(position: Int64(index * 4 * 2))
|
||||
let _ = strongSelf.file.seek(position: Int64(index * 4 * 2))
|
||||
var offset = Int32(currentSize)
|
||||
var length = Int32(compressedData.count)
|
||||
let _ = strongSelf.file.write(&offset, count: 4)
|
||||
let _ = strongSelf.file.write(&length, count: 4)
|
||||
strongSelf.file.seek(position: Int64(currentSize))
|
||||
let _ = strongSelf.file.seek(position: Int64(currentSize))
|
||||
compressedData.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) -> Void in
|
||||
if let baseAddress = buffer.baseAddress {
|
||||
let _ = strongSelf.file.write(baseAddress, count: Int(length))
|
||||
@ -502,7 +502,7 @@ private final class AnimatedStickerDirectFrameSourceCache {
|
||||
|
||||
switch rangeResult {
|
||||
case let .range(range):
|
||||
self.file.seek(position: Int64(range.lowerBound))
|
||||
let _ = self.file.seek(position: Int64(range.lowerBound))
|
||||
let length = range.upperBound - range.lowerBound
|
||||
let compressedData = self.file.readData(count: length)
|
||||
if compressedData.count != length {
|
||||
|
@ -100,7 +100,7 @@ private final class VideoStickerFrameSourceCache {
|
||||
return true
|
||||
}
|
||||
|
||||
self.file.seek(position: 0)
|
||||
let _ = self.file.seek(position: 0)
|
||||
var frameRate: Int32 = 0
|
||||
if self.file.read(&frameRate, 4) != 4 {
|
||||
return false
|
||||
@ -113,7 +113,7 @@ private final class VideoStickerFrameSourceCache {
|
||||
}
|
||||
self.frameRate = frameRate
|
||||
|
||||
self.file.seek(position: 4)
|
||||
let _ = self.file.seek(position: 4)
|
||||
|
||||
var frameCount: Int32 = 0
|
||||
if self.file.read(&frameCount, 4) != 4 {
|
||||
@ -144,7 +144,7 @@ private final class VideoStickerFrameSourceCache {
|
||||
return .notFound
|
||||
}
|
||||
|
||||
self.file.seek(position: Int64(8 + index * 4 * 2))
|
||||
let _ = self.file.seek(position: Int64(8 + index * 4 * 2))
|
||||
var offset: Int32 = 0
|
||||
var length: Int32 = 0
|
||||
if self.file.read(&offset, 4) != 4 {
|
||||
@ -167,11 +167,11 @@ private final class VideoStickerFrameSourceCache {
|
||||
}
|
||||
|
||||
func storeFrameRateAndCount(frameRate: Int, frameCount: Int) {
|
||||
self.file.seek(position: 0)
|
||||
let _ = self.file.seek(position: 0)
|
||||
var frameRate = Int32(frameRate)
|
||||
let _ = self.file.write(&frameRate, count: 4)
|
||||
|
||||
self.file.seek(position: 4)
|
||||
let _ = self.file.seek(position: 4)
|
||||
var frameCount = Int32(frameCount)
|
||||
let _ = self.file.write(&frameCount, count: 4)
|
||||
}
|
||||
@ -203,12 +203,12 @@ private final class VideoStickerFrameSourceCache {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.file.seek(position: Int64(8 + index * 4 * 2))
|
||||
let _ = strongSelf.file.seek(position: Int64(8 + index * 4 * 2))
|
||||
var offset = Int32(currentSize)
|
||||
var length = Int32(compressedData.count)
|
||||
let _ = strongSelf.file.write(&offset, count: 4)
|
||||
let _ = strongSelf.file.write(&length, count: 4)
|
||||
strongSelf.file.seek(position: Int64(currentSize))
|
||||
let _ = strongSelf.file.seek(position: Int64(currentSize))
|
||||
compressedData.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) -> Void in
|
||||
if let baseAddress = buffer.baseAddress {
|
||||
let _ = strongSelf.file.write(baseAddress, count: Int(length))
|
||||
@ -226,7 +226,7 @@ private final class VideoStickerFrameSourceCache {
|
||||
|
||||
switch rangeResult {
|
||||
case let .range(range):
|
||||
self.file.seek(position: Int64(range.lowerBound))
|
||||
let _ = self.file.seek(position: Int64(range.lowerBound))
|
||||
let length = range.upperBound - range.lowerBound
|
||||
let compressedData = self.file.readData(count: length)
|
||||
if compressedData.count != length {
|
||||
|
@ -177,12 +177,13 @@ private func generateMaskImage() -> UIImage? {
|
||||
public class AttachmentController: ViewController {
|
||||
private let context: AccountContext
|
||||
private let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
|
||||
private let chatLocation: ChatLocation
|
||||
private let chatLocation: ChatLocation?
|
||||
private let buttons: [AttachmentButtonType]
|
||||
private let initialButton: AttachmentButtonType
|
||||
private let fromMenu: Bool
|
||||
private let hasTextInput: Bool
|
||||
private let makeEntityInputView: () -> AttachmentTextInputPanelInputView?
|
||||
public var animateAppearance: Bool = false
|
||||
|
||||
public var willDismiss: () -> Void = {}
|
||||
public var didDismiss: () -> Void = {}
|
||||
@ -619,14 +620,26 @@ public class AttachmentController: ViewController {
|
||||
|
||||
private var animating = false
|
||||
func animateIn() {
|
||||
guard let layout = self.validLayout else {
|
||||
guard let layout = self.validLayout, let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
|
||||
self.animating = true
|
||||
if case .regular = layout.metrics.widthClass {
|
||||
self.animating = false
|
||||
|
||||
if controller.animateAppearance {
|
||||
let targetPosition = self.position
|
||||
let startPosition = targetPosition.offsetBy(dx: 0.0, dy: layout.size.height)
|
||||
|
||||
self.position = startPosition
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
||||
transition.animateView(allowUserInteraction: true, {
|
||||
self.position = targetPosition
|
||||
}, completion: { _ in
|
||||
self.animating = false
|
||||
})
|
||||
} else {
|
||||
self.animating = false
|
||||
}
|
||||
ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear).updateAlpha(node: self.dim, alpha: 0.1)
|
||||
} else {
|
||||
ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear).updateAlpha(node: self.dim, alpha: 1.0)
|
||||
@ -888,7 +901,7 @@ public class AttachmentController: ViewController {
|
||||
|
||||
public var getSourceRect: (() -> CGRect?)?
|
||||
|
||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, chatLocation: ChatLocation, buttons: [AttachmentButtonType], initialButton: AttachmentButtonType = .gallery, fromMenu: Bool = false, hasTextInput: Bool = true, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView? = { return nil}) {
|
||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, chatLocation: ChatLocation?, buttons: [AttachmentButtonType], initialButton: AttachmentButtonType = .gallery, fromMenu: Bool = false, hasTextInput: Bool = true, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView? = { return nil}) {
|
||||
self.context = context
|
||||
self.updatedPresentationData = updatedPresentationData
|
||||
self.chatLocation = chatLocation
|
||||
|
@ -387,7 +387,13 @@ public struct AttachmentMainButtonState {
|
||||
case center
|
||||
}
|
||||
|
||||
public enum Font: Equatable {
|
||||
case regular
|
||||
case bold
|
||||
}
|
||||
|
||||
public let text: String?
|
||||
public let font: Font
|
||||
public let background: Background
|
||||
public let textColor: UIColor
|
||||
public let isVisible: Bool
|
||||
@ -396,6 +402,7 @@ public struct AttachmentMainButtonState {
|
||||
|
||||
public init(
|
||||
text: String?,
|
||||
font: Font,
|
||||
background: Background,
|
||||
textColor: UIColor,
|
||||
isVisible: Bool,
|
||||
@ -403,6 +410,7 @@ public struct AttachmentMainButtonState {
|
||||
isEnabled: Bool
|
||||
) {
|
||||
self.text = text
|
||||
self.font = font
|
||||
self.background = background
|
||||
self.textColor = textColor
|
||||
self.isVisible = isVisible
|
||||
@ -411,7 +419,7 @@ public struct AttachmentMainButtonState {
|
||||
}
|
||||
|
||||
static var initial: AttachmentMainButtonState {
|
||||
return AttachmentMainButtonState(text: nil, background: .color(.clear), textColor: .clear, isVisible: false, progress: .none, isEnabled: false)
|
||||
return AttachmentMainButtonState(text: nil, font: .bold, background: .color(.clear), textColor: .clear, isVisible: false, progress: .none, isEnabled: false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -443,6 +451,7 @@ private final class MainButtonNode: HighlightTrackingButtonNode {
|
||||
|
||||
super.init(pointerStyle: pointerStyle)
|
||||
|
||||
self.isExclusiveTouch = true
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.addSubnode(self.backgroundAnimationNode)
|
||||
@ -643,7 +652,14 @@ private final class MainButtonNode: HighlightTrackingButtonNode {
|
||||
self.setupShimmering()
|
||||
|
||||
if let text = state.text {
|
||||
self.textNode.attributedText = NSAttributedString(string: text, font: Font.semibold(17.0), textColor: state.textColor)
|
||||
let font: UIFont
|
||||
switch state.font {
|
||||
case .regular:
|
||||
font = Font.regular(17.0)
|
||||
case .bold:
|
||||
font = Font.semibold(17.0)
|
||||
}
|
||||
self.textNode.attributedText = NSAttributedString(string: text, font: font, textColor: state.textColor)
|
||||
|
||||
let textSize = self.textNode.updateLayout(size)
|
||||
self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize)
|
||||
@ -758,13 +774,13 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
var mainButtonPressed: () -> Void = { }
|
||||
|
||||
init(context: AccountContext, chatLocation: ChatLocation, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) {
|
||||
init(context: AccountContext, chatLocation: ChatLocation?, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) {
|
||||
self.context = context
|
||||
self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
self.makeEntityInputView = makeEntityInputView
|
||||
|
||||
self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(previewing: false), chatLocation: chatLocation, subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil)
|
||||
self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(previewing: false), chatLocation: chatLocation ?? .peer(id: context.account.peerId), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil)
|
||||
|
||||
self.containerNode = ASDisplayNode()
|
||||
self.containerNode.clipsToBounds = true
|
||||
@ -936,7 +952,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
||||
return
|
||||
}
|
||||
textInputPanelNode.loadTextInputNodeIfNeeded()
|
||||
guard let textInputNode = textInputPanelNode.textInputNode, let peerId = chatLocation.peerId else {
|
||||
guard let textInputNode = textInputPanelNode.textInputNode, let peerId = chatLocation?.peerId else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -1267,7 +1283,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
||||
func updateMainButtonState(_ mainButtonState: AttachmentMainButtonState?) {
|
||||
var currentButtonState = self.mainButtonState
|
||||
if mainButtonState == nil {
|
||||
currentButtonState = AttachmentMainButtonState(text: currentButtonState.text, background: currentButtonState.background, textColor: currentButtonState.textColor, isVisible: false, progress: .none, isEnabled: currentButtonState.isEnabled)
|
||||
currentButtonState = AttachmentMainButtonState(text: currentButtonState.text, font: currentButtonState.font, background: currentButtonState.background, textColor: currentButtonState.textColor, isVisible: false, progress: .none, isEnabled: currentButtonState.isEnabled)
|
||||
}
|
||||
self.mainButtonState = mainButtonState ?? currentButtonState
|
||||
}
|
||||
@ -1417,6 +1433,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
||||
self.scrollNode.isUserInteractionEnabled = !isSelecting
|
||||
|
||||
let isButtonVisible = self.mainButtonState.isVisible
|
||||
let isNarrowButton = isButtonVisible && self.mainButtonState.font == .regular
|
||||
|
||||
var insets = layout.insets(options: [])
|
||||
if let inputHeight = layout.inputHeight, inputHeight > 0.0 && (isSelecting || isButtonVisible) {
|
||||
@ -1452,12 +1469,12 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
|
||||
let bounds = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: buttonSize.height + insets.bottom))
|
||||
let containerTransition: ContainedViewLayoutTransition
|
||||
var containerTransition: ContainedViewLayoutTransition
|
||||
let containerFrame: CGRect
|
||||
if isButtonVisible {
|
||||
var height: CGFloat
|
||||
if layout.intrinsicInsets.bottom > 0.0 && (layout.inputHeight ?? 0.0).isZero {
|
||||
height = bounds.height + 9.0
|
||||
height = bounds.height
|
||||
if case .regular = layout.metrics.widthClass {
|
||||
if self.isStandalone {
|
||||
height -= 3.0
|
||||
@ -1466,7 +1483,10 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
height = bounds.height + 9.0 + 8.0
|
||||
height = bounds.height + 8.0
|
||||
}
|
||||
if !isNarrowButton {
|
||||
height += 9.0
|
||||
}
|
||||
containerFrame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: height))
|
||||
} else if isSelecting {
|
||||
@ -1500,6 +1520,10 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
if self.containerNode.frame.size.width.isZero {
|
||||
containerTransition = .immediate
|
||||
}
|
||||
|
||||
containerTransition.updateFrame(node: self.containerNode, frame: containerFrame)
|
||||
containerTransition.updateFrame(node: self.backgroundNode, frame: containerBounds)
|
||||
self.backgroundNode.update(size: containerBounds.size, transition: transition)
|
||||
@ -1532,11 +1556,13 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
let sideInset: CGFloat = 16.0
|
||||
let buttonSize = CGSize(width: layout.size.width - (sideInset + layout.safeInsets.left) * 2.0, height: 50.0)
|
||||
let buttonTopInset: CGFloat = isNarrowButton ? 2.0 : 8.0
|
||||
|
||||
if !self.dismissed {
|
||||
self.mainButtonNode.updateLayout(size: buttonSize, state: self.mainButtonState, transition: transition)
|
||||
}
|
||||
if !self.animatingTransition {
|
||||
transition.updateFrame(node: self.mainButtonNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + sideInset, y: isButtonVisible || self.fromMenu ? 8.0 : containerFrame.height), size: buttonSize))
|
||||
transition.updateFrame(node: self.mainButtonNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + sideInset, y: isButtonVisible || self.fromMenu ? buttonTopInset : containerFrame.height), size: buttonSize))
|
||||
}
|
||||
|
||||
return containerFrame.height
|
||||
|
@ -7,6 +7,8 @@ load(
|
||||
"telegram_is_appstore_build",
|
||||
"telegram_appstore_id",
|
||||
"telegram_app_specific_url_scheme",
|
||||
"telegram_enable_icloud",
|
||||
"telegram_enable_siri",
|
||||
)
|
||||
|
||||
objc_library(
|
||||
@ -25,6 +27,8 @@ objc_library(
|
||||
"-DAPP_CONFIG_IS_APPSTORE_BUILD={}".format(telegram_is_appstore_build),
|
||||
"-DAPP_CONFIG_APPSTORE_ID={}".format(telegram_appstore_id),
|
||||
"-DAPP_SPECIFIC_URL_SCHEME=\\\"{}\\\"".format(telegram_app_specific_url_scheme),
|
||||
"-DAPP_CONFIG_IS_ICLOUD_ENABLED={}".format("true" if telegram_enable_icloud else "false"),
|
||||
"-DAPP_CONFIG_IS_SIRI_ENABLED={}".format("true" if telegram_enable_siri else "false"),
|
||||
],
|
||||
hdrs = glob([
|
||||
"PublicHeaders/**/*.h",
|
||||
|
@ -18,6 +18,8 @@
|
||||
@property (nonatomic, readonly) bool isAppStoreBuild;
|
||||
@property (nonatomic, readonly) int64_t appStoreId;
|
||||
@property (nonatomic, strong, readonly) NSString * _Nonnull appSpecificUrlScheme;
|
||||
@property (nonatomic, readonly) bool isICloudEnabled;
|
||||
@property (nonatomic, readonly) bool isSiriEnabled;
|
||||
|
||||
+ (DeviceSpecificEncryptionParameters * _Nonnull)deviceSpecificEncryptionParameters:(NSString * _Nonnull)rootPath baseAppBundleId:(NSString * _Nonnull)baseAppBundleId;
|
||||
- (NSData * _Nullable)bundleDataWithAppToken:(NSData * _Nullable)appToken signatureDict:(NSDictionary * _Nullable)signatureDict;
|
||||
|
@ -185,6 +185,14 @@ API_AVAILABLE(ios(10))
|
||||
return @(APP_SPECIFIC_URL_SCHEME);
|
||||
}
|
||||
|
||||
- (bool)isICloudEnabled {
|
||||
return APP_CONFIG_IS_ICLOUD_ENABLED;
|
||||
}
|
||||
|
||||
- (bool)isSiriEnabled {
|
||||
return APP_CONFIG_IS_SIRI_ENABLED;
|
||||
}
|
||||
|
||||
+ (NSString * _Nullable)bundleSeedId {
|
||||
NSDictionary *query = [NSDictionary dictionaryWithObjectsAndKeys:
|
||||
(__bridge NSString *)kSecClassGenericPassword, (__bridge NSString *)kSecClass,
|
||||
|
@ -1370,7 +1370,7 @@ public final class CalendarMessageScreen: ViewController {
|
||||
if self.selectionState?.dayRange == nil {
|
||||
if let selectionToolbarNode = self.selectionToolbarNode {
|
||||
let toolbarFrame = selectionToolbarNode.view.convert(selectionToolbarNode.bounds, to: self.view)
|
||||
self.controller?.present(TooltipScreen(account: self.context.account, text: self.presentationData.strings.MessageCalendar_EmptySelectionTooltip, style: .default, icon: .none, location: .point(toolbarFrame.insetBy(dx: 0.0, dy: 10.0), .bottom), shouldDismissOnTouch: { point in
|
||||
self.controller?.present(TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: self.presentationData.strings.MessageCalendar_EmptySelectionTooltip, style: .default, icon: .none, location: .point(toolbarFrame.insetBy(dx: 0.0, dy: 10.0), .bottom), shouldDismissOnTouch: { point in
|
||||
return .dismiss(consume: false)
|
||||
}), in: .current)
|
||||
}
|
||||
|
@ -1599,12 +1599,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
})))
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
|
||||
for filter in filters {
|
||||
if filter.id == filterId, case let .filter(_, title, _, data) = filter {
|
||||
if let filterPeersAreMuted {
|
||||
items.append(.action(ContextMenuActionItem(text: filterPeersAreMuted.areMuted ? "Unmute All" : "Mute All", textColor: .primary, badge: nil, icon: { theme in
|
||||
items.append(.action(ContextMenuActionItem(text: filterPeersAreMuted.areMuted ? strongSelf.presentationData.strings.ChatList_ContextUnmuteAll : strongSelf.presentationData.strings.ChatList_ContextMuteAll, textColor: .primary, badge: nil, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: filterPeersAreMuted.areMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { c, f in
|
||||
c.dismiss(completion: {
|
||||
@ -1623,21 +1621,23 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
let iconColor: UIColor = .white
|
||||
let overlayController: UndoOverlayController
|
||||
if !filterPeersAreMuted.areMuted {
|
||||
let text = strongSelf.presentationData.strings.ChatList_ToastFolderMuted(title).string
|
||||
overlayController = UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_profilemute", scale: 0.075, colors: [
|
||||
"Middle.Group 1.Fill 1": iconColor,
|
||||
"Top.Group 1.Fill 1": iconColor,
|
||||
"Bottom.Group 1.Fill 1": iconColor,
|
||||
"EXAMPLE.Group 1.Fill 1": iconColor,
|
||||
"Line.Group 1.Stroke 1": iconColor
|
||||
], title: nil, text: "All chats in **\(title)** are now muted", customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false })
|
||||
], title: nil, text: text, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false })
|
||||
} else {
|
||||
let text = strongSelf.presentationData.strings.ChatList_ToastFolderUnmuted(title).string
|
||||
overlayController = UndoOverlayController(presentationData: strongSelf.presentationData, content: .universal(animation: "anim_profileunmute", scale: 0.075, colors: [
|
||||
"Middle.Group 1.Fill 1": iconColor,
|
||||
"Top.Group 1.Fill 1": iconColor,
|
||||
"Bottom.Group 1.Fill 1": iconColor,
|
||||
"EXAMPLE.Group 1.Fill 1": iconColor,
|
||||
"Line.Group 1.Stroke 1": iconColor
|
||||
], title: nil, text: "All chats in **\(title)** are now unmuted", customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false })
|
||||
], title: nil, text: text, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: true, action: { _ in return false })
|
||||
}
|
||||
strongSelf.present(overlayController, in: .current)
|
||||
})
|
||||
@ -1645,7 +1645,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
|
||||
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: data.hasSharedLinks ? nil : ContextMenuActionBadge(value: "NEW", color: .accent, style: .label), icon: { theme in
|
||||
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.ChatList_ContextMenuShare, textColor: .primary, badge: data.hasSharedLinks ? nil : ContextMenuActionBadge(value: strongSelf.presentationData.strings.ChatList_ContextMenuBadgeNew, color: .accent, style: .label), icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { c, f in
|
||||
c.dismiss(completion: {
|
||||
@ -2000,6 +2000,35 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
let _ = value
|
||||
})
|
||||
})
|
||||
|
||||
Queue.mainQueue().after(2.0, { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
//TODO:generalize
|
||||
var hasEmptyMark = false
|
||||
self.chatListDisplayNode.mainContainerNode.currentItemNode.forEachItemNode { itemNode in
|
||||
if itemNode is ChatListSectionHeaderNode {
|
||||
hasEmptyMark = true
|
||||
}
|
||||
}
|
||||
if hasEmptyMark {
|
||||
if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
|
||||
if let rightButtonView = componentView.rightButtonView {
|
||||
let absoluteFrame = rightButtonView.convert(rightButtonView.bounds, to: self.view)
|
||||
let text: String = self.presentationData.strings.ChatList_EmptyListTooltip
|
||||
|
||||
let tooltipController = TooltipController(content: .text(text), baseFontSize: self.presentationData.listsFontSize.baseDisplaySize, timeout: 30.0, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true, padding: 6.0, innerPadding: UIEdgeInsets(top: 2.0, left: 3.0, bottom: 2.0, right: 3.0))
|
||||
self.present(tooltipController, in: .current, with: TooltipControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
return (self.displayNode, absoluteFrame.insetBy(dx: 4.0, dy: 8.0).offsetBy(dx: 4.0, dy: -1.0))
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
self.chatListDisplayNode.mainContainerNode.addedVisibleChatsWithPeerIds = { [weak self] peerIds in
|
||||
@ -2079,7 +2108,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
|
||||
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 8.0), size: CGSize())
|
||||
|
||||
parentController.present(TooltipScreen(account: strongSelf.context.account, text: text, icon: .chatListPress, location: .point(location, .bottom), shouldDismissOnTouch: { point in
|
||||
parentController.present(TooltipScreen(account: strongSelf.context.account, sharedContext: strongSelf.context.sharedContext, text: text, icon: .chatListPress, location: .point(location, .bottom), shouldDismissOnTouch: { point in
|
||||
guard let strongSelf = self, let parentController = strongSelf.parent as? TabBarController else {
|
||||
return .dismiss(consume: false)
|
||||
}
|
||||
@ -2997,8 +3026,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
|
||||
if hasLinks {
|
||||
//TODO:localize
|
||||
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: "Delete Folder", text: "Are you sure you want to delete this folder? This will also deactivate all the invite links used to share this folder.", actions: [
|
||||
self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.ChatList_AlertDeleteFolderTitle, text: presentationData.strings.ChatList_AlertDeleteFolderText, actions: [
|
||||
TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: {
|
||||
confirmDeleteFolder()
|
||||
}),
|
||||
|
@ -571,13 +571,7 @@ private final class ChatListContainerItemNode: ASDisplayNode {
|
||||
self.topPanel = topPanel
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
let title: String
|
||||
if chatFolderUpdates.availableChatsToJoin == 1 {
|
||||
title = "1 New Chat Available"
|
||||
} else {
|
||||
title = "\(chatFolderUpdates.availableChatsToJoin) New Chats Available"
|
||||
}
|
||||
let title: String = self.presentationData.strings.ChatList_PanelNewChatsAvailable(Int32(chatFolderUpdates.availableChatsToJoin))
|
||||
|
||||
let topPanelHeight: CGFloat = 44.0
|
||||
|
||||
|
@ -547,11 +547,9 @@ private enum ChatListFilterPresetEntry: ItemListNodeEntry {
|
||||
arguments.expandSection(.exclude)
|
||||
})
|
||||
case let .inviteLinkHeader(hasLinks):
|
||||
//TODO:localize
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: "SHARE FOLDER", badge: hasLinks ? nil : "NEW", sectionId: self.section)
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.ChatListFilter_SectionShare, badge: hasLinks ? nil : presentationData.strings.ChatList_ContextMenuBadgeNew, sectionId: self.section)
|
||||
case let .inviteLinkCreate(hasLinks):
|
||||
//TODO:localize
|
||||
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.linkIcon(presentationData.theme), title: hasLinks ? "Create a new Link" : "Create an Invite Link", sectionId: self.section, editing: false, action: {
|
||||
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.linkIcon(presentationData.theme), title: hasLinks ? presentationData.strings.ChatListFilter_CreateLink : presentationData.strings.ChatListFilter_CreateLinkNew, sectionId: self.section, editing: false, action: {
|
||||
arguments.createLink()
|
||||
})
|
||||
case let .inviteLink(_, link):
|
||||
@ -648,8 +646,7 @@ private func chatListFilterPresetControllerEntries(presentationData: Presentatio
|
||||
if let currentPreset, let data = currentPreset.data, data.isShared {
|
||||
} else {
|
||||
entries.append(.excludePeersHeader(presentationData.strings.ChatListFolder_ExcludedSectionHeader))
|
||||
//TODO:localize
|
||||
entries.append(.addExcludePeer(title: "Add Chats to Exclude"))
|
||||
entries.append(.addExcludePeer(title: presentationData.strings.ChatListFilter_ExcludeChatsAction))
|
||||
|
||||
var excludeCategoryIndex = 0
|
||||
for category in ChatListFilterExcludeCategory.allCases {
|
||||
@ -705,8 +702,7 @@ private func chatListFilterPresetControllerEntries(presentationData: Presentatio
|
||||
}
|
||||
}
|
||||
|
||||
//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."))
|
||||
entries.append(.inviteLinkInfo(text: hasLinks ? presentationData.strings.ChatListFilter_LinkListInfo : presentationData.strings.ChatListFilter_LinkListInfoNew))
|
||||
|
||||
return entries
|
||||
}
|
||||
@ -840,37 +836,23 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
|
||||
}
|
||||
includePeers.sort()
|
||||
|
||||
let newPeers = includePeers.filter({ !(filter.data?.includePeers.peers.contains($0) ?? false) })
|
||||
var removedPeers: [PeerId] = []
|
||||
if let data = filter.data {
|
||||
removedPeers = data.includePeers.peers.filter({ !includePeers.contains($0) })
|
||||
}
|
||||
if newPeers.count != 0 {
|
||||
let title: String
|
||||
let text: String
|
||||
|
||||
if newPeers.count == 1 {
|
||||
title = "Сhat added to folder"
|
||||
text = "It will not affect chatlist of the links of this folder"
|
||||
} else {
|
||||
title = "\(newPeers.count) chats added to folder"
|
||||
text = "It will not affect chatlist of the links of this folder"
|
||||
if filter.id > 1, case let .filter(_, _, _, data) = filter, data.hasSharedLinks {
|
||||
let newPeers = includePeers.filter({ !(filter.data?.includePeers.peers.contains($0) ?? false) })
|
||||
var removedPeers: [PeerId] = []
|
||||
if let data = filter.data {
|
||||
removedPeers = data.includePeers.peers.filter({ !includePeers.contains($0) })
|
||||
}
|
||||
|
||||
presentUndo(.info(title: title, text: text, timeout: nil))
|
||||
} else if removedPeers.count != 0 {
|
||||
let title: String
|
||||
let text: String
|
||||
|
||||
if newPeers.count == 1 {
|
||||
title = "Сhat removed from folder"
|
||||
text = "It will not affect chatlist of the links of this folder"
|
||||
} else {
|
||||
title = "\(newPeers.count) chats removed from folder"
|
||||
text = "It will not affect chatlist of the links of this folder"
|
||||
if newPeers.count != 0 {
|
||||
let title: String = presentationData.strings.ChatListFilter_ToastChatsAddedTitle(Int32(newPeers.count))
|
||||
let text: String = presentationData.strings.ChatListFilter_ToastChatsAddedText
|
||||
|
||||
presentUndo(.universal(animation: "anim_add_to_folder", scale: 0.1, colors: ["__allcolors__": UIColor.white], title: title, text: text, customUndoText: nil, timeout: nil))
|
||||
} else if removedPeers.count != 0 {
|
||||
let title: String = presentationData.strings.ChatListFilter_ToastChatsRemovedTitle(Int32(newPeers.count))
|
||||
let text: String = presentationData.strings.ChatListFilter_ToastChatsRemovedText
|
||||
|
||||
presentUndo(.universal(animation: "anim_remove_from_folder", scale: 0.1, colors: ["__allcolors__": UIColor.white], title: title, text: text, customUndoText: nil, timeout: nil))
|
||||
}
|
||||
|
||||
presentUndo(.info(title: title, text: text, timeout: nil))
|
||||
}
|
||||
|
||||
var categories: ChatListFilterPeerCategories = []
|
||||
@ -1321,6 +1303,20 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
let _ = (updatedCurrentPreset |> take(1) |> deliverOnMainQueue).start(next: { currentPreset in
|
||||
if let currentPreset, let data = currentPreset.data, data.hasSharedLinks {
|
||||
let title: String
|
||||
let text: String
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
title = presentationData.strings.ChatListFilter_ToastChatsRemovedTitle(1)
|
||||
text = presentationData.strings.ChatListFilter_ToastChatsRemovedText
|
||||
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_remove_from_folder", scale: 0.1, colors: ["__allcolors__": UIColor.white], title: title, text: text, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
}
|
||||
})
|
||||
},
|
||||
deleteExcludePeer: { peerId in
|
||||
updateState { state in
|
||||
@ -1376,17 +1372,15 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
|
||||
},
|
||||
createLink: {
|
||||
if initialPreset == nil {
|
||||
//TODO:localize
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let text = "Please finish creating this folder to share it."
|
||||
let text = presentationData.strings.ChatListFilter_AlertCreateFolderBeforeSharingText
|
||||
presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||||
} else {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let state = stateValue.with({ $0 })
|
||||
if state.additionallyIncludePeers.isEmpty {
|
||||
//TODO:localize
|
||||
let text = "You can’t share folders which have chat types or excluded chats."
|
||||
let text = presentationData.strings.ChatListFilter_ErrorShareInvalidFolder
|
||||
presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
|
||||
|
||||
return
|
||||
@ -1400,14 +1394,13 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
|
||||
|
||||
let _ = (updatedCurrentPreset |> take(1) |> deliverOnMainQueue).start(next: { currentPreset in
|
||||
if let currentPreset, let data = currentPreset.data {
|
||||
//TODO:localize
|
||||
var unavailableText: String?
|
||||
if !data.categories.isEmpty {
|
||||
unavailableText = "You can’t share folders which have chat types or excluded chats."
|
||||
unavailableText = presentationData.strings.ChatListFilter_ErrorShareInvalidFolder
|
||||
} else if data.excludeArchived || data.excludeRead || data.excludeMuted {
|
||||
unavailableText = "You can’t share folders which have chat types or excluded chats."
|
||||
unavailableText = presentationData.strings.ChatListFilter_ErrorShareInvalidFolder
|
||||
} else if !data.excludePeers.isEmpty {
|
||||
unavailableText = "You can’t share folders which have chat types or excluded chats."
|
||||
unavailableText = presentationData.strings.ChatListFilter_ErrorShareInvalidFolder
|
||||
}
|
||||
if let unavailableText {
|
||||
statusController?.dismiss()
|
||||
@ -1581,6 +1574,20 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
let _ = (updatedCurrentPreset |> take(1) |> deliverOnMainQueue).start(next: { currentPreset in
|
||||
if let currentPreset, let data = currentPreset.data, data.hasSharedLinks {
|
||||
let title: String
|
||||
let text: String
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
title = presentationData.strings.ChatListFilter_ToastChatsRemovedTitle(1)
|
||||
text = presentationData.strings.ChatListFilter_ToastChatsRemovedText
|
||||
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_remove_from_folder", scale: 0.1, colors: ["__allcolors__": UIColor.white], title: title, text: text, customUndoText: nil, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
}
|
||||
})
|
||||
})))
|
||||
|
||||
let contextController = ContextController(account: context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
|
||||
@ -1757,8 +1764,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
|
||||
TextAlertAction(type: .genericAction, title: presentationData.strings.ChatListFolder_DiscardDiscard, action: {
|
||||
dismissImpl?()
|
||||
}),
|
||||
//TODO:localize
|
||||
TextAlertAction(type: .defaultAction, title: "Save", action: {
|
||||
TextAlertAction(type: .defaultAction, title: presentationData.strings.ChatListFilter_SaveAlertActionSave, action: {
|
||||
applyImpl?(false, {
|
||||
dismissImpl?()
|
||||
})
|
||||
@ -1899,11 +1905,13 @@ func openCreateChatListFolderLink(context: AccountContext, folderId: Int32, chec
|
||||
}))
|
||||
}, error: { error in
|
||||
completed()
|
||||
//TODO:localize
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let text: String
|
||||
switch error {
|
||||
case .generic:
|
||||
text = "An error occurred"
|
||||
text = presentationData.strings.ChatListFilter_CreateLinkUnknownError
|
||||
case let .sharedFolderLimitExceeded(limit, _):
|
||||
let limitController = context.sharedContext.makePremiumLimitController(context: context, subject: .membershipInSharedFolders, count: limit, action: {
|
||||
pushPremiumController(PremiumIntroScreen(context: context, source: .membershipInSharedFolders))
|
||||
@ -1933,10 +1941,8 @@ func openCreateChatListFolderLink(context: AccountContext, folderId: Int32, chec
|
||||
|
||||
return
|
||||
case .someUserTooManyChannels:
|
||||
//TODO:localize
|
||||
text = "One of the groups in this folder can’t be added because one of its admins has too many groups and channels."
|
||||
text = presentationData.strings.ChatListFilter_CreateLinkErrorSomeoneHasChannelLimit
|
||||
}
|
||||
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: {})]))
|
||||
})
|
||||
}
|
||||
|
@ -223,8 +223,7 @@ private func chatListFilterPresetListControllerEntries(presentationData: Present
|
||||
|
||||
entries.append(.listHeader(presentationData.strings.ChatListFolderSettings_FoldersSection))
|
||||
|
||||
//TODO:localize
|
||||
entries.append(.addItem(text: "Create a Folder", isEditing: state.isEditing))
|
||||
entries.append(.addItem(text: presentationData.strings.ChatListFilterList_CreateFolder, isEditing: state.isEditing))
|
||||
|
||||
if !filters.isEmpty || suggestedFilters.isEmpty {
|
||||
var folderCount = 0
|
||||
@ -450,8 +449,7 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
|
||||
}
|
||||
|
||||
if hasLinks {
|
||||
//TODO:localize
|
||||
presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: "Delete Folder", text: "Are you sure you want to delete this folder? This will also deactivate all the invite links used to share this folder.", actions: [
|
||||
presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.ChatList_AlertDeleteFolderTitle, text: presentationData.strings.ChatList_AlertDeleteFolderText, actions: [
|
||||
TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: {
|
||||
confirmDeleteFolder()
|
||||
}),
|
||||
|
277
submodules/ChatListUI/Sources/Node/ChatListEmptyInfoItem.swift
Normal file
277
submodules/ChatListUI/Sources/Node/ChatListEmptyInfoItem.swift
Normal file
@ -0,0 +1,277 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import ListSectionHeaderNode
|
||||
import AppBundle
|
||||
import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
|
||||
class ChatListEmptyInfoItem: ListViewItem {
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
|
||||
let selectable: Bool = false
|
||||
|
||||
init(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = ChatListEmptyInfoItemNode()
|
||||
|
||||
let (nodeLayout, apply) = node.asyncLayout()(self, params, false)
|
||||
|
||||
node.insets = nodeLayout.insets
|
||||
node.contentSize = nodeLayout.contentSize
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in
|
||||
apply()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
assert(node() is ChatListEmptyInfoItemNode)
|
||||
if let nodeValue = node() as? ChatListEmptyInfoItemNode {
|
||||
|
||||
let layout = nodeValue.asyncLayout()
|
||||
async {
|
||||
let (nodeLayout, apply) = layout(self, params, nextItem == nil)
|
||||
Queue.mainQueue().async {
|
||||
completion(nodeLayout, { _ in
|
||||
apply()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ChatListEmptyInfoItemNode: ListViewItemNode {
|
||||
private var item: ChatListEmptyInfoItem?
|
||||
|
||||
private let animationNode: AnimatedStickerNode
|
||||
private let textNode: TextNode
|
||||
|
||||
override var visibility: ListViewItemNodeVisibility {
|
||||
didSet {
|
||||
let wasVisible = self.visibilityStatus
|
||||
let isVisible: Bool
|
||||
switch self.visibility {
|
||||
case let .visible(fraction, _):
|
||||
isVisible = fraction > 0.2
|
||||
case .none:
|
||||
isVisible = false
|
||||
}
|
||||
if wasVisible != isVisible {
|
||||
self.visibilityStatus = isVisible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var visibilityStatus: Bool = false {
|
||||
didSet {
|
||||
if self.visibilityStatus != oldValue {
|
||||
self.animationNode.visibility = self.visibilityStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init() {
|
||||
self.animationNode = DefaultAnimatedStickerNodeImpl()
|
||||
self.textNode = TextNode()
|
||||
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.addSubnode(self.animationNode)
|
||||
self.addSubnode(self.textNode)
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
}
|
||||
|
||||
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
|
||||
let layout = self.asyncLayout()
|
||||
let (_, apply) = layout(item as! ChatListEmptyInfoItem, params, nextItem == nil)
|
||||
apply()
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: ChatListEmptyInfoItem, _ params: ListViewItemLayoutParams, _ isLast: Bool) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTextLayout = TextNode.asyncLayout(self.textNode)
|
||||
|
||||
return { item, params, last in
|
||||
let baseWidth = params.width - params.leftInset - params.rightInset
|
||||
|
||||
let topInset: CGFloat = 8.0
|
||||
let textSpacing: CGFloat = 27.0
|
||||
let bottomInset: CGFloat = 24.0
|
||||
let animationHeight: CGFloat = 140.0
|
||||
|
||||
let string = NSMutableAttributedString(string: item.strings.ChatList_EmptyChatList, font: Font.semibold(17.0), textColor: item.theme.list.itemPrimaryTextColor)
|
||||
|
||||
let textLayout = makeTextLayout(TextNodeLayoutArguments(attributedString: string, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: baseWidth, height: .greatestFiniteMagnitude), alignment: .center))
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: topInset + animationHeight + textSpacing + textLayout.0.size.height + bottomInset), insets: UIEdgeInsets())
|
||||
|
||||
return (layout, { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.item = item
|
||||
|
||||
var topOffset: CGFloat = topInset
|
||||
|
||||
let animationFrame = CGRect(origin: CGPoint(x: floor((params.width - animationHeight) * 0.5), y: topOffset), size: CGSize(width: animationHeight, height: animationHeight))
|
||||
if strongSelf.animationNode.bounds.isEmpty {
|
||||
strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "ChatListEmpty"), width: 248, height: 248, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
||||
}
|
||||
strongSelf.animationNode.frame = animationFrame
|
||||
topOffset += animationHeight + textSpacing
|
||||
|
||||
let _ = textLayout.1()
|
||||
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: floor((params.width - textLayout.0.size.width) * 0.5), y: topOffset), size: textLayout.0.size)
|
||||
|
||||
strongSelf.contentSize = layout.contentSize
|
||||
strongSelf.insets = layout.insets
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ChatListSectionHeaderItem: ListViewItem {
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let hide: (() -> Void)?
|
||||
|
||||
let selectable: Bool = false
|
||||
|
||||
init(theme: PresentationTheme, strings: PresentationStrings, hide: (() -> Void)?) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.hide = hide
|
||||
}
|
||||
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
||||
async {
|
||||
let node = ChatListSectionHeaderNode()
|
||||
|
||||
let (nodeLayout, apply) = node.asyncLayout()(self, params, false)
|
||||
|
||||
node.insets = nodeLayout.insets
|
||||
node.contentSize = nodeLayout.contentSize
|
||||
|
||||
Queue.mainQueue().async {
|
||||
completion(node, {
|
||||
return (nil, { _ in
|
||||
apply()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
|
||||
Queue.mainQueue().async {
|
||||
assert(node() is ChatListSectionHeaderNode)
|
||||
if let nodeValue = node() as? ChatListSectionHeaderNode {
|
||||
|
||||
let layout = nodeValue.asyncLayout()
|
||||
async {
|
||||
let (nodeLayout, apply) = layout(self, params, nextItem == nil)
|
||||
Queue.mainQueue().async {
|
||||
completion(nodeLayout, { _ in
|
||||
apply()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ChatListSectionHeaderNode: ListViewItemNode {
|
||||
private var item: ChatListSectionHeaderItem?
|
||||
|
||||
private var headerNode: ListSectionHeaderNode?
|
||||
|
||||
required init() {
|
||||
super.init(layerBacked: false, dynamicBounce: false)
|
||||
|
||||
self.zPosition = 1.0
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
}
|
||||
|
||||
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
|
||||
let layout = self.asyncLayout()
|
||||
let (_, apply) = layout(item as! ChatListSectionHeaderItem, params, nextItem == nil)
|
||||
apply()
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if let headerNode = self.headerNode {
|
||||
if let result = headerNode.view.hitTest(self.view.convert(point, to: headerNode.view), with: event) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: ChatListSectionHeaderItem, _ params: ListViewItemLayoutParams, _ isLast: Bool) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
return { item, params, last in
|
||||
let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 28.0), insets: UIEdgeInsets())
|
||||
|
||||
return (layout, { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.item = item
|
||||
|
||||
let headerNode: ListSectionHeaderNode
|
||||
if let current = strongSelf.headerNode {
|
||||
headerNode = current
|
||||
} else {
|
||||
headerNode = ListSectionHeaderNode(theme: item.theme)
|
||||
strongSelf.headerNode = headerNode
|
||||
strongSelf.addSubnode(headerNode)
|
||||
}
|
||||
|
||||
headerNode.title = item.strings.ChatList_EmptyListContactsHeader
|
||||
if item.hide != nil {
|
||||
headerNode.action = item.strings.ChatList_EmptyListContactsHeaderHide
|
||||
headerNode.actionType = .generic
|
||||
headerNode.activateAction = {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.item?.hide?()
|
||||
}
|
||||
} else {
|
||||
headerNode.action = nil
|
||||
}
|
||||
|
||||
headerNode.updateTheme(theme: item.theme)
|
||||
headerNode.updateLayout(size: CGSize(width: params.width, height: layout.contentSize.height), leftInset: params.leftInset, rightInset: params.rightInset)
|
||||
|
||||
strongSelf.contentSize = layout.contentSize
|
||||
strongSelf.insets = layout.insets
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3312,6 +3312,12 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
|
||||
var nextTitleIconOrigin: CGFloat = contentRect.origin.x + titleLayout.trailingLineWidth + 3.0 + titleOffset
|
||||
let lastLineRect: CGRect
|
||||
if let rect = titleLayout.linesRects().last {
|
||||
lastLineRect = CGRect(origin: CGPoint(x: 0.0, y: titleLayout.size.height - rect.height - 2.0), size: CGSize(width: rect.width, height: rect.height + 2.0))
|
||||
} else {
|
||||
lastLineRect = CGRect(origin: CGPoint(), size: titleLayout.size)
|
||||
}
|
||||
|
||||
if let currentCredibilityIconContent = currentCredibilityIconContent {
|
||||
let credibilityIconView: ComponentHostView<Empty>
|
||||
@ -3339,7 +3345,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 20.0, height: 20.0)
|
||||
)
|
||||
transition.updateFrame(view: credibilityIconView, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin, y: floorToScreenPixels(titleFrame.midY - iconSize.height / 2.0) - UIScreenPixel), size: iconSize))
|
||||
transition.updateFrame(view: credibilityIconView, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin, y: floorToScreenPixels(titleFrame.maxY - lastLineRect.height * 0.5 - iconSize.height / 2.0) - UIScreenPixel), size: iconSize))
|
||||
nextTitleIconOrigin += credibilityIconView.bounds.width + 4.0
|
||||
} else if let credibilityIconView = strongSelf.credibilityIconView {
|
||||
strongSelf.credibilityIconView = nil
|
||||
@ -3349,7 +3355,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
||||
if let currentMutedIconImage = currentMutedIconImage {
|
||||
strongSelf.mutedIconNode.image = currentMutedIconImage
|
||||
strongSelf.mutedIconNode.isHidden = false
|
||||
transition.updateFrame(node: strongSelf.mutedIconNode, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin - 5.0, y: floorToScreenPixels(titleFrame.midY - currentMutedIconImage.size.height / 2.0)), size: currentMutedIconImage.size))
|
||||
transition.updateFrame(node: strongSelf.mutedIconNode, frame: CGRect(origin: CGPoint(x: nextTitleIconOrigin - 5.0, y: floorToScreenPixels(titleFrame.maxY - lastLineRect.height * 0.5 - currentMutedIconImage.size.height / 2.0)), size: currentMutedIconImage.size))
|
||||
nextTitleIconOrigin += currentMutedIconImage.size.width + 1.0
|
||||
} else {
|
||||
strongSelf.mutedIconNode.image = nil
|
||||
|
@ -616,8 +616,44 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
hiddenOffset: hiddenByDefault && !revealed,
|
||||
interaction: nodeInteraction
|
||||
), directionHint: entry.directionHint)
|
||||
case let .ContactEntry(contactEntry):
|
||||
let header: ChatListSearchItemHeader? = nil
|
||||
|
||||
var status: ContactsPeerItemStatus = .none
|
||||
status = .presence(contactEntry.presence, contactEntry.presentationData.dateTimeFormat)
|
||||
|
||||
let presentationData = contactEntry.presentationData
|
||||
|
||||
let peerContent: ContactsPeerItemPeer = .peer(peer: contactEntry.peer, chatPeer: contactEntry.peer)
|
||||
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(
|
||||
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder),
|
||||
sortOrder: presentationData.nameSortOrder,
|
||||
displayOrder: presentationData.nameDisplayOrder,
|
||||
context: context,
|
||||
peerMode: .generalSearch,
|
||||
peer: peerContent,
|
||||
status: status,
|
||||
enabled: true,
|
||||
selection: .none,
|
||||
editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false),
|
||||
index: nil,
|
||||
header: header,
|
||||
action: { _ in
|
||||
nodeInteraction.peerSelected(contactEntry.peer, nil, nil, nil)
|
||||
},
|
||||
disabledAction: nil,
|
||||
animationCache: nodeInteraction.animationCache,
|
||||
animationRenderer: nodeInteraction.animationRenderer
|
||||
), directionHint: entry.directionHint)
|
||||
case let .ArchiveIntro(presentationData):
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListArchiveInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint)
|
||||
case let .EmptyIntro(presentationData):
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint)
|
||||
case let .SectionHeader(presentationData, displayHide):
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSectionHeaderItem(theme: presentationData.theme, strings: presentationData.strings, hide: displayHide ? {
|
||||
hideChatListContacts(context: context)
|
||||
} : nil), directionHint: entry.directionHint)
|
||||
case let .Notice(presentationData, notice):
|
||||
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListStorageInfoItem(theme: presentationData.theme, strings: presentationData.strings, notice: notice, action: { [weak nodeInteraction] action in
|
||||
switch action {
|
||||
@ -881,8 +917,44 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
|
||||
hiddenOffset: hiddenByDefault && !revealed,
|
||||
interaction: nodeInteraction
|
||||
), directionHint: entry.directionHint)
|
||||
case let .ContactEntry(contactEntry):
|
||||
let header: ChatListSearchItemHeader? = nil
|
||||
|
||||
var status: ContactsPeerItemStatus = .none
|
||||
status = .presence(contactEntry.presence, contactEntry.presentationData.dateTimeFormat)
|
||||
|
||||
let presentationData = contactEntry.presentationData
|
||||
|
||||
let peerContent: ContactsPeerItemPeer = .peer(peer: contactEntry.peer, chatPeer: contactEntry.peer)
|
||||
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(
|
||||
presentationData: ItemListPresentationData(theme: presentationData.theme, fontSize: presentationData.fontSize, strings: presentationData.strings, nameDisplayOrder: presentationData.nameDisplayOrder),
|
||||
sortOrder: presentationData.nameSortOrder,
|
||||
displayOrder: presentationData.nameDisplayOrder,
|
||||
context: context,
|
||||
peerMode: .generalSearch,
|
||||
peer: peerContent,
|
||||
status: status,
|
||||
enabled: true,
|
||||
selection: .none,
|
||||
editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false),
|
||||
index: nil,
|
||||
header: header,
|
||||
action: { _ in
|
||||
nodeInteraction.peerSelected(contactEntry.peer, nil, nil, nil)
|
||||
},
|
||||
disabledAction: nil,
|
||||
animationCache: nodeInteraction.animationCache,
|
||||
animationRenderer: nodeInteraction.animationRenderer
|
||||
), directionHint: entry.directionHint)
|
||||
case let .ArchiveIntro(presentationData):
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListArchiveInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint)
|
||||
case let .EmptyIntro(presentationData):
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyInfoItem(theme: presentationData.theme, strings: presentationData.strings), directionHint: entry.directionHint)
|
||||
case let .SectionHeader(presentationData, displayHide):
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSectionHeaderItem(theme: presentationData.theme, strings: presentationData.strings, hide: displayHide ? {
|
||||
hideChatListContacts(context: context)
|
||||
} : nil), directionHint: entry.directionHint)
|
||||
case let .Notice(presentationData, notice):
|
||||
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListStorageInfoItem(theme: presentationData.theme, strings: presentationData.strings, notice: notice, action: { [weak nodeInteraction] action in
|
||||
switch action {
|
||||
@ -1702,6 +1774,65 @@ public final class ChatListNode: ListView {
|
||||
let _ = self.enqueueTransition(value).start()
|
||||
})*/
|
||||
|
||||
let contacts: Signal<[ChatListContactPeer], NoError>
|
||||
if case .chatList(groupId: .root) = location, chatListFilter == nil {
|
||||
contacts = ApplicationSpecificNotice.displayChatListContacts(accountManager: context.sharedContext.accountManager)
|
||||
|> distinctUntilChanged
|
||||
|> mapToSignal { value -> Signal<[ChatListContactPeer], NoError> in
|
||||
if value {
|
||||
return .single([])
|
||||
}
|
||||
|
||||
return context.engine.messages.chatList(group: .root, count: 10)
|
||||
|> map { chatList -> Bool in
|
||||
if chatList.items.count >= 5 {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|> mapToSignal { hasChats -> Signal<[ChatListContactPeer], NoError> in
|
||||
if hasChats {
|
||||
return .single([])
|
||||
}
|
||||
|
||||
return context.engine.data.subscribe(
|
||||
TelegramEngine.EngineData.Item.Contacts.List(includePresences: true)
|
||||
)
|
||||
|> mapToThrottled { next -> Signal<EngineContactList, NoError> in
|
||||
return .single(next)
|
||||
|> then(
|
||||
.complete()
|
||||
|> delay(5.0, queue: Queue.concurrentDefaultQueue())
|
||||
)
|
||||
}
|
||||
|> map { contactList -> [ChatListContactPeer] in
|
||||
var result: [ChatListContactPeer] = []
|
||||
for peer in contactList.peers {
|
||||
if peer.id == context.account.peerId {
|
||||
continue
|
||||
}
|
||||
result.append(ChatListContactPeer(
|
||||
peer: peer,
|
||||
presence: contactList.presences[peer.id] ?? EnginePeer.Presence(status: .longTimeAgo, lastActivity: 0)
|
||||
))
|
||||
}
|
||||
result.sort(by: { lhs, rhs in
|
||||
if lhs.presence.status != rhs.presence.status {
|
||||
return lhs.presence.status < rhs.presence.status
|
||||
} else {
|
||||
return lhs.peer.id < rhs.peer.id
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
contacts = .single([])
|
||||
}
|
||||
|
||||
let chatListNodeViewTransition = combineLatest(
|
||||
queue: viewProcessingQueue,
|
||||
hideArchivedFolderByDefault,
|
||||
@ -1711,9 +1842,10 @@ public final class ChatListNode: ListView {
|
||||
savedMessagesPeer,
|
||||
chatListViewUpdate,
|
||||
self.chatFolderUpdates.get() |> distinctUntilChanged,
|
||||
self.statePromise.get()
|
||||
self.statePromise.get(),
|
||||
contacts
|
||||
)
|
||||
|> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestedChatListNotice, savedMessagesPeer, updateAndFilter, chatFolderUpdates, state) -> Signal<ChatListNodeListViewTransition, NoError> in
|
||||
|> mapToQueue { (hideArchivedFolderByDefault, displayArchiveIntro, storageInfo, suggestedChatListNotice, savedMessagesPeer, updateAndFilter, chatFolderUpdates, state, contacts) -> Signal<ChatListNodeListViewTransition, NoError> in
|
||||
let (update, filter) = updateAndFilter
|
||||
|
||||
let previousHideArchivedFolderByDefaultValue = previousHideArchivedFolderByDefault.swap(hideArchivedFolderByDefault)
|
||||
@ -1729,7 +1861,7 @@ public final class ChatListNode: ListView {
|
||||
notice = nil
|
||||
}
|
||||
|
||||
let (rawEntries, isLoading) = chatListNodeEntriesForView(update.list, state: state, savedMessagesPeer: savedMessagesPeer, foundPeers: state.foundPeers, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, notice: notice, mode: mode, chatListLocation: location)
|
||||
let (rawEntries, isLoading) = chatListNodeEntriesForView(update.list, state: state, savedMessagesPeer: savedMessagesPeer, foundPeers: state.foundPeers, hideArchivedFolderByDefault: hideArchivedFolderByDefault, displayArchiveIntro: displayArchiveIntro, notice: notice, mode: mode, chatListLocation: location, contacts: contacts)
|
||||
var isEmpty = true
|
||||
var entries = rawEntries.filter { entry in
|
||||
switch entry {
|
||||
@ -1974,6 +2106,9 @@ public final class ChatListNode: ListView {
|
||||
return false
|
||||
}
|
||||
}
|
||||
case .ContactEntry:
|
||||
isEmpty = false
|
||||
return true
|
||||
case .GroupReferenceEntry:
|
||||
isEmpty = false
|
||||
return true
|
||||
@ -2910,7 +3045,7 @@ public final class ChatListNode: ListView {
|
||||
var hasArchive = false
|
||||
loop: for entry in transition.chatListView.filteredEntries {
|
||||
switch entry {
|
||||
case .GroupReferenceEntry, .HoleEntry, .PeerEntry:
|
||||
case .GroupReferenceEntry, .HoleEntry, .PeerEntry, .ContactEntry:
|
||||
if case .GroupReferenceEntry = entry {
|
||||
hasArchive = true
|
||||
} else {
|
||||
@ -2929,7 +3064,7 @@ public final class ChatListNode: ListView {
|
||||
} else {
|
||||
break loop
|
||||
}
|
||||
case .ArchiveIntro, .Notice, .HeaderEntry, .AdditionalCategory:
|
||||
case .ArchiveIntro, .EmptyIntro, .SectionHeader, .Notice, .HeaderEntry, .AdditionalCategory:
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -3660,3 +3795,7 @@ public class ChatHistoryListSelectionRecognizer: UIPanGestureRecognizer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hideChatListContacts(context: AccountContext) {
|
||||
let _ = ApplicationSpecificNotice.setDisplayChatListContacts(accountManager: context.sharedContext.accountManager).start()
|
||||
}
|
||||
|
@ -12,7 +12,10 @@ enum ChatListNodeEntryId: Hashable {
|
||||
case PeerId(Int64)
|
||||
case ThreadId(Int64)
|
||||
case GroupId(EngineChatList.Group)
|
||||
case ContactId(EnginePeer.Id)
|
||||
case ArchiveIntro
|
||||
case EmptyIntro
|
||||
case SectionHeader
|
||||
case Notice
|
||||
case additionalCategory(Int)
|
||||
}
|
||||
@ -20,6 +23,8 @@ enum ChatListNodeEntryId: Hashable {
|
||||
enum ChatListNodeEntrySortIndex: Comparable {
|
||||
case index(EngineChatList.Item.Index)
|
||||
case additionalCategory(Int)
|
||||
case sectionHeader
|
||||
case contact(id: EnginePeer.Id, presence: EnginePeer.Presence)
|
||||
|
||||
static func <(lhs: ChatListNodeEntrySortIndex, rhs: ChatListNodeEntrySortIndex) -> Bool {
|
||||
switch lhs {
|
||||
@ -29,6 +34,10 @@ enum ChatListNodeEntrySortIndex: Comparable {
|
||||
return lhsIndex < rhsIndex
|
||||
case .additionalCategory:
|
||||
return false
|
||||
case .sectionHeader:
|
||||
return true
|
||||
case .contact:
|
||||
return true
|
||||
}
|
||||
case let .additionalCategory(lhsIndex):
|
||||
switch rhs {
|
||||
@ -36,6 +45,30 @@ enum ChatListNodeEntrySortIndex: Comparable {
|
||||
return lhsIndex < rhsIndex
|
||||
case .index:
|
||||
return true
|
||||
case .sectionHeader:
|
||||
return true
|
||||
case .contact:
|
||||
return true
|
||||
}
|
||||
case .sectionHeader:
|
||||
switch rhs {
|
||||
case .additionalCategory, .index, .sectionHeader:
|
||||
return false
|
||||
case .contact:
|
||||
return true
|
||||
}
|
||||
case let .contact(lhsId, lhsPresense):
|
||||
switch rhs {
|
||||
case .sectionHeader:
|
||||
return false
|
||||
case let .contact(rhsId, rhsPresense):
|
||||
if lhsPresense != rhsPresense {
|
||||
return rhsPresense.status > rhsPresense.status
|
||||
} else {
|
||||
return lhsId < rhsId
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -238,11 +271,39 @@ enum ChatListNodeEntry: Comparable, Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
struct ContactEntryData: Equatable {
|
||||
var presentationData: ChatListPresentationData
|
||||
var peer: EnginePeer
|
||||
var presence: EnginePeer.Presence
|
||||
|
||||
init(presentationData: ChatListPresentationData, peer: EnginePeer, presence: EnginePeer.Presence) {
|
||||
self.presentationData = presentationData
|
||||
self.peer = peer
|
||||
self.presence = presence
|
||||
}
|
||||
|
||||
static func ==(lhs: ContactEntryData, rhs: ContactEntryData) -> Bool {
|
||||
if lhs.presentationData !== rhs.presentationData {
|
||||
return false
|
||||
}
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
if lhs.presence != rhs.presence {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
case HeaderEntry
|
||||
case PeerEntry(PeerEntryData)
|
||||
case HoleEntry(EngineMessage.Index, theme: PresentationTheme)
|
||||
case GroupReferenceEntry(index: EngineChatList.Item.Index, presentationData: ChatListPresentationData, groupId: EngineChatList.Group, peers: [EngineChatList.GroupItem.Item], message: EngineMessage?, editing: Bool, unreadCount: Int, revealed: Bool, hiddenByDefault: Bool)
|
||||
case ContactEntry(ContactEntryData)
|
||||
case ArchiveIntro(presentationData: ChatListPresentationData)
|
||||
case EmptyIntro(presentationData: ChatListPresentationData)
|
||||
case SectionHeader(presentationData: ChatListPresentationData, displayHide: Bool)
|
||||
case Notice(presentationData: ChatListPresentationData, notice: ChatListNotice)
|
||||
case AdditionalCategory(index: Int, id: Int, title: String, image: UIImage?, appearance: ChatListNodeAdditionalCategory.Appearance, selected: Bool, presentationData: ChatListPresentationData)
|
||||
|
||||
@ -256,8 +317,14 @@ enum ChatListNodeEntry: Comparable, Identifiable {
|
||||
return .index(.chatList(EngineChatList.Item.Index.ChatList(pinningIndex: nil, messageIndex: holeIndex)))
|
||||
case let .GroupReferenceEntry(index, _, _, _, _, _, _, _, _):
|
||||
return .index(index)
|
||||
case let .ContactEntry(contactEntry):
|
||||
return .contact(id: contactEntry.peer.id, presence: contactEntry.presence)
|
||||
case .ArchiveIntro:
|
||||
return .index(.chatList(EngineChatList.Item.Index.ChatList.absoluteUpperBound.successor))
|
||||
case .EmptyIntro:
|
||||
return .index(.chatList(EngineChatList.Item.Index.ChatList.absoluteUpperBound.successor))
|
||||
case .SectionHeader:
|
||||
return .sectionHeader
|
||||
case .Notice:
|
||||
return .index(.chatList(EngineChatList.Item.Index.ChatList.absoluteUpperBound.successor.successor))
|
||||
case let .AdditionalCategory(index, _, _, _, _, _, _):
|
||||
@ -280,8 +347,14 @@ enum ChatListNodeEntry: Comparable, Identifiable {
|
||||
return .Hole(Int64(holeIndex.id.id))
|
||||
case let .GroupReferenceEntry(_, _, groupId, _, _, _, _, _, _):
|
||||
return .GroupId(groupId)
|
||||
case let .ContactEntry(contactEntry):
|
||||
return .ContactId(contactEntry.peer.id)
|
||||
case .ArchiveIntro:
|
||||
return .ArchiveIntro
|
||||
case .EmptyIntro:
|
||||
return .EmptyIntro
|
||||
case .SectionHeader:
|
||||
return .SectionHeader
|
||||
case .Notice:
|
||||
return .Notice
|
||||
case let .AdditionalCategory(_, id, _, _, _, _, _):
|
||||
@ -347,6 +420,12 @@ enum ChatListNodeEntry: Comparable, Identifiable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .ContactEntry(contactEntry):
|
||||
if case .ContactEntry(contactEntry) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .ArchiveIntro(lhsPresentationData):
|
||||
if case let .ArchiveIntro(rhsPresentationData) = rhs {
|
||||
if lhsPresentationData !== rhsPresentationData {
|
||||
@ -356,6 +435,27 @@ enum ChatListNodeEntry: Comparable, Identifiable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .EmptyIntro(lhsPresentationData):
|
||||
if case let .EmptyIntro(rhsPresentationData) = rhs {
|
||||
if lhsPresentationData !== rhsPresentationData {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .SectionHeader(lhsPresentationData, lhsDisplayHide):
|
||||
if case let .SectionHeader(rhsPresentationData, rhsDisplayHide) = rhs {
|
||||
if lhsPresentationData !== rhsPresentationData {
|
||||
return false
|
||||
}
|
||||
if lhsDisplayHide != rhsDisplayHide {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .Notice(lhsPresentationData, lhsInfo):
|
||||
if case let .Notice(rhsPresentationData, rhsInfo) = rhs {
|
||||
if lhsPresentationData !== rhsPresentationData {
|
||||
@ -407,9 +507,32 @@ private func offsetPinnedIndex(_ index: EngineChatList.Item.Index, offset: UInt1
|
||||
}
|
||||
}
|
||||
|
||||
func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState, savedMessagesPeer: EnginePeer?, foundPeers: [(EnginePeer, EnginePeer?)], hideArchivedFolderByDefault: Bool, displayArchiveIntro: Bool, notice: ChatListNotice?, mode: ChatListNodeMode, chatListLocation: ChatListControllerLocation) -> (entries: [ChatListNodeEntry], loading: Bool) {
|
||||
struct ChatListContactPeer {
|
||||
var peer: EnginePeer
|
||||
var presence: EnginePeer.Presence
|
||||
|
||||
init(peer: EnginePeer, presence: EnginePeer.Presence) {
|
||||
self.peer = peer
|
||||
self.presence = presence
|
||||
}
|
||||
}
|
||||
|
||||
func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState, savedMessagesPeer: EnginePeer?, foundPeers: [(EnginePeer, EnginePeer?)], hideArchivedFolderByDefault: Bool, displayArchiveIntro: Bool, notice: ChatListNotice?, mode: ChatListNodeMode, chatListLocation: ChatListControllerLocation, contacts: [ChatListContactPeer]) -> (entries: [ChatListNodeEntry], loading: Bool) {
|
||||
var result: [ChatListNodeEntry] = []
|
||||
|
||||
if !view.hasEarlier {
|
||||
for contact in contacts {
|
||||
result.append(.ContactEntry(ChatListNodeEntry.ContactEntryData(
|
||||
presentationData: state.presentationData,
|
||||
peer: contact.peer,
|
||||
presence: contact.presence
|
||||
)))
|
||||
}
|
||||
if !contacts.isEmpty {
|
||||
result.append(.SectionHeader(presentationData: state.presentationData, displayHide: !view.items.isEmpty))
|
||||
}
|
||||
}
|
||||
|
||||
var pinnedIndexOffset: UInt16 = 0
|
||||
|
||||
if !view.hasLater, case .chatList = mode {
|
||||
@ -668,6 +791,14 @@ func chatListNodeEntriesForView(_ view: EngineChatList, state: ChatListNodeState
|
||||
|
||||
if displayArchiveIntro {
|
||||
result.append(.ArchiveIntro(presentationData: state.presentationData))
|
||||
} else if !contacts.isEmpty && !result.contains(where: { entry in
|
||||
if case .PeerEntry = entry {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}) {
|
||||
result.append(.EmptyIntro(presentationData: state.presentationData))
|
||||
}
|
||||
|
||||
if let notice {
|
||||
|
@ -221,8 +221,7 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode {
|
||||
|
||||
switch item.notice {
|
||||
case .chatFolderUpdates:
|
||||
//TODO:locallize
|
||||
strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: "Hide", icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)]))
|
||||
strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.strings.ChatList_HideAction, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)]))
|
||||
default:
|
||||
strongSelf.setRevealOptions((left: [], right: []))
|
||||
}
|
||||
|
@ -138,6 +138,7 @@ public final class NavigationBackgroundNode: ASDisplayNode {
|
||||
private var _color: UIColor
|
||||
|
||||
private var enableBlur: Bool
|
||||
private var enableSaturation: Bool
|
||||
|
||||
public var effectView: UIVisualEffectView?
|
||||
private let backgroundNode: ASDisplayNode
|
||||
@ -152,9 +153,10 @@ public final class NavigationBackgroundNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
public init(color: UIColor, enableBlur: Bool = true) {
|
||||
public init(color: UIColor, enableBlur: Bool = true, enableSaturation: Bool = true) {
|
||||
self._color = .clear
|
||||
self.enableBlur = enableBlur
|
||||
self.enableSaturation = enableSaturation
|
||||
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
|
||||
@ -195,10 +197,12 @@ public final class NavigationBackgroundNode: ASDisplayNode {
|
||||
if let sublayer = effectView.layer.sublayers?[0], let filters = sublayer.filters {
|
||||
sublayer.backgroundColor = nil
|
||||
sublayer.isOpaque = false
|
||||
let allowedKeys: [String] = [
|
||||
"colorSaturate",
|
||||
var allowedKeys: [String] = [
|
||||
"gaussianBlur"
|
||||
]
|
||||
if self.enableSaturation {
|
||||
allowedKeys.append("colorSaturate")
|
||||
}
|
||||
sublayer.filters = filters.filter { filter in
|
||||
guard let filter = filter as? NSObject else {
|
||||
return true
|
||||
@ -225,14 +229,16 @@ public final class NavigationBackgroundNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
public func updateColor(color: UIColor, enableBlur: Bool? = nil, forceKeepBlur: Bool = false, transition: ContainedViewLayoutTransition) {
|
||||
public func updateColor(color: UIColor, enableBlur: Bool? = nil, enableSaturation: Bool? = nil, forceKeepBlur: Bool = false, transition: ContainedViewLayoutTransition) {
|
||||
let effectiveEnableBlur = enableBlur ?? self.enableBlur
|
||||
|
||||
if self._color.isEqual(color) && self.enableBlur == effectiveEnableBlur {
|
||||
let effectiveEnableSaturation = enableSaturation ?? self.enableSaturation
|
||||
|
||||
if self._color.isEqual(color) && self.enableBlur == effectiveEnableBlur && self.enableSaturation == effectiveEnableSaturation {
|
||||
return
|
||||
}
|
||||
self._color = color
|
||||
self.enableBlur = effectiveEnableBlur
|
||||
self.enableSaturation = effectiveEnableSaturation
|
||||
|
||||
if sharedIsReduceTransparencyEnabled {
|
||||
transition.updateBackgroundColor(node: self.backgroundNode, color: self._color.withAlphaComponent(1.0))
|
||||
|
@ -123,13 +123,14 @@ open class TooltipController: ViewController, StandalonePresentableController {
|
||||
private var timeoutTimer: SwiftSignalKit.Timer?
|
||||
|
||||
private var padding: CGFloat
|
||||
private var innerPadding: UIEdgeInsets
|
||||
|
||||
private var layout: ContainerViewLayout?
|
||||
private var initialArrowOnBottom: Bool
|
||||
|
||||
public var dismissed: ((Bool) -> Void)?
|
||||
|
||||
public init(content: TooltipControllerContent, baseFontSize: CGFloat, timeout: Double = 2.0, dismissByTapOutside: Bool = false, dismissByTapOutsideSource: Bool = false, dismissImmediatelyOnLayoutUpdate: Bool = false, arrowOnBottom: Bool = true, padding: CGFloat = 8.0) {
|
||||
public init(content: TooltipControllerContent, baseFontSize: CGFloat, timeout: Double = 2.0, dismissByTapOutside: Bool = false, dismissByTapOutsideSource: Bool = false, dismissImmediatelyOnLayoutUpdate: Bool = false, arrowOnBottom: Bool = true, padding: CGFloat = 8.0, innerPadding: UIEdgeInsets = UIEdgeInsets()) {
|
||||
self.content = content
|
||||
self.baseFontSize = baseFontSize
|
||||
self.timeout = timeout
|
||||
@ -138,6 +139,7 @@ open class TooltipController: ViewController, StandalonePresentableController {
|
||||
self.dismissImmediatelyOnLayoutUpdate = dismissImmediatelyOnLayoutUpdate
|
||||
self.initialArrowOnBottom = arrowOnBottom
|
||||
self.padding = padding
|
||||
self.innerPadding = innerPadding
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
@ -157,6 +159,7 @@ open class TooltipController: ViewController, StandalonePresentableController {
|
||||
self?.dismiss(tappedInside: tappedInside)
|
||||
}, dismissByTapOutside: self.dismissByTapOutside, dismissByTapOutsideSource: self.dismissByTapOutsideSource)
|
||||
self.controllerNode.padding = self.padding
|
||||
self.controllerNode.innerPadding = self.innerPadding
|
||||
self.controllerNode.arrowOnBottom = self.initialArrowOnBottom
|
||||
self.displayNodeDidLoad()
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ final class TooltipControllerNode: ASDisplayNode {
|
||||
var arrowOnBottom: Bool = true
|
||||
|
||||
var padding: CGFloat = 8.0
|
||||
var innerPadding: UIEdgeInsets = UIEdgeInsets()
|
||||
|
||||
private var dismissedByTouchOutside = false
|
||||
private var dismissByTapOutsideSource = false
|
||||
@ -98,14 +99,14 @@ final class TooltipControllerNode: ASDisplayNode {
|
||||
textSize.width = ceil(textSize.width / 2.0) * 2.0
|
||||
textSize.height = ceil(textSize.height / 2.0) * 2.0
|
||||
|
||||
contentSize = CGSize(width: imageSizeWithInset.width + textSize.width + 12.0, height: textSize.height + 34.0)
|
||||
contentSize = CGSize(width: imageSizeWithInset.width + textSize.width + 12.0 + self.innerPadding.left + self.innerPadding.right, height: textSize.height + 34.0 + self.innerPadding.top + self.innerPadding.bottom)
|
||||
|
||||
let textFrame = CGRect(origin: CGPoint(x: 6.0 + imageSizeWithInset.width, y: 17.0), size: textSize)
|
||||
let textFrame = CGRect(origin: CGPoint(x: 6.0 + self.innerPadding.left + imageSizeWithInset.width, y: 17.0 + self.innerPadding.top), size: textSize)
|
||||
if transition.isAnimated, textFrame.size != self.textNode.frame.size {
|
||||
transition.animatePositionAdditive(node: self.textNode, offset: CGPoint(x: textFrame.minX - self.textNode.frame.minX, y: 0.0))
|
||||
}
|
||||
|
||||
let imageFrame = CGRect(origin: CGPoint(x: 10.0, y: floor((contentSize.height - imageSize.height) / 2.0)), size: imageSize)
|
||||
let imageFrame = CGRect(origin: CGPoint(x: self.innerPadding.left + 10.0, y: floor((contentSize.height - imageSize.height) / 2.0)), size: imageSize)
|
||||
self.imageNode.frame = imageFrame
|
||||
self.textNode.frame = textFrame
|
||||
}
|
||||
|
@ -1211,7 +1211,9 @@ public class GalleryController: ViewController, StandalonePresentableController,
|
||||
})
|
||||
}
|
||||
})
|
||||
self.displayNode = GalleryControllerNode(controllerInteraction: controllerInteraction)
|
||||
|
||||
let disableTapNavigation = !(self.context.sharedContext.currentMediaDisplaySettings.with { $0 }.showNextMediaOnTap)
|
||||
self.displayNode = GalleryControllerNode(controllerInteraction: controllerInteraction, disableTapNavigation: disableTapNavigation)
|
||||
self.displayNodeDidLoad()
|
||||
|
||||
self.galleryNode.statusBar = self.statusBar
|
||||
|
@ -585,7 +585,7 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGest
|
||||
let node = self.itemNodes[i]
|
||||
transition.updateFrame(node: node, frame: CGRect(origin: CGPoint(x: CGFloat(i) * self.scrollView.bounds.size.width + self.pageGap, y: 0.0), size: CGSize(width: self.scrollView.bounds.size.width - self.pageGap * 2.0, height: self.scrollView.bounds.size.height)))
|
||||
|
||||
let screenFrame = node.convert(node.bounds, to: self.supernode)
|
||||
let screenFrame = node.view.convert(node.view.bounds, to: self.view.superview)
|
||||
node.screenFrameUpdated(screenFrame)
|
||||
}
|
||||
|
||||
|
@ -39,6 +39,7 @@ public func blurredImage(_ image: UIImage, radius: CGFloat, iterations: Int = 3)
|
||||
let source = CFDataGetBytePtr(providerData)
|
||||
memcpy(inBuffer.data, source, bytes)
|
||||
|
||||
|
||||
for _ in 0 ..< iterations {
|
||||
vImageBoxConvolve_ARGB8888(&inBuffer, &outBuffer, tempData, 0, 0, boxSize, boxSize, nil, vImage_Flags(kvImageEdgeExtend))
|
||||
|
||||
|
@ -170,7 +170,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
|
||||
case let .mainLinkHeader(text):
|
||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||
case let .mainLink(link, isGenerating):
|
||||
return ItemListFolderInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: link, count: 0, peers: [], displayButton: true, enableButton: !isGenerating, buttonTitle: link != nil ? "Copy" : "Generate Invite Link", secondaryButtonTitle: link != nil ? "Share" : nil, displayImporters: false, buttonColor: nil, sectionId: self.section, style: .blocks, copyAction: {
|
||||
return ItemListFolderInviteLinkItem(context: arguments.context, presentationData: presentationData, invite: link, count: 0, peers: [], displayButton: true, enableButton: !isGenerating, buttonTitle: presentationData.strings.FolderLinkScreen_LinkActionCopy, secondaryButtonTitle: link != nil ? presentationData.strings.FolderLinkScreen_LinkActionShare : nil, displayImporters: false, buttonColor: nil, sectionId: self.section, style: .blocks, copyAction: {
|
||||
if let link {
|
||||
arguments.copyLink(link.link)
|
||||
}
|
||||
@ -196,7 +196,6 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
|
||||
case let .peersInfo(text):
|
||||
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
|
||||
case let .peer(_, peer, isSelected, disabledReasonText):
|
||||
//TODO:localize
|
||||
return ItemListPeerItem(
|
||||
presentationData: presentationData,
|
||||
dateTimeFormat: PresentationDateTimeFormat(),
|
||||
@ -204,7 +203,7 @@ private enum InviteLinksListEntry: ItemListNodeEntry {
|
||||
context: arguments.context,
|
||||
peer: peer,
|
||||
presence: nil,
|
||||
text: .text(disabledReasonText ?? "you can invite others here", .secondary),
|
||||
text: .text(disabledReasonText ?? presentationData.strings.FolderLinkScreen_LabelCanInvite, .secondary),
|
||||
label: .none,
|
||||
editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false),
|
||||
switchValue: ItemListPeerItemSwitch(value: isSelected, style: .leftCheck, isEnabled: disabledReasonText == nil),
|
||||
@ -232,8 +231,6 @@ private func folderInviteLinkListControllerEntries(
|
||||
) -> [InviteLinksListEntry] {
|
||||
var entries: [InviteLinksListEntry] = []
|
||||
|
||||
//TODO:localize
|
||||
|
||||
var infoString: String?
|
||||
let chatCountString: String
|
||||
let peersHeaderString: String
|
||||
@ -244,34 +241,26 @@ private func folderInviteLinkListControllerEntries(
|
||||
var selectAllString: String?
|
||||
|
||||
if !canShareChats {
|
||||
infoString = "You can only share groups and channels in which you are allowed to create invite links."
|
||||
chatCountString = "There are no chats in this folder that you can share with others."
|
||||
peersHeaderString = "THESE CHATS CANNOT BE SHARED"
|
||||
infoString = presentationData.strings.FolderLinkScreen_TitleDescriptionUnavailable
|
||||
chatCountString = presentationData.strings.FolderLinkScreen_ChatCountHeaderUnavailable
|
||||
peersHeaderString = presentationData.strings.FolderLinkScreen_ChatsSectionHeaderUnavailable
|
||||
} else if state.selectedPeerIds.isEmpty {
|
||||
chatCountString = "Anyone with this link can add **\(title)** folder and the chats selected below."
|
||||
peersHeaderString = "CHATS"
|
||||
chatCountString = presentationData.strings.FolderLinkScreen_TitleDescriptionDeselected(title).string
|
||||
peersHeaderString = presentationData.strings.FolderLinkScreen_ChatsSectionHeader
|
||||
if allPeers.count > 1 {
|
||||
selectAllString = allSelected ? "DESELECT ALL" : "SELECT ALL"
|
||||
}
|
||||
} else if state.selectedPeerIds.count == 1 {
|
||||
chatCountString = "Anyone with this link can add **\(title)** folder and the 1 chat selected below."
|
||||
peersHeaderString = "1 CHAT SELECTED"
|
||||
if allPeers.count > 1 {
|
||||
selectAllString = allSelected ? "DESELECT ALL" : "SELECT ALL"
|
||||
selectAllString = allSelected ? presentationData.strings.FolderLinkScreen_ChatsSectionHeaderActionDeselectAll : presentationData.strings.FolderLinkScreen_ChatsSectionHeaderActionSelectAll
|
||||
}
|
||||
} else {
|
||||
chatCountString = "Anyone with this link can add **\(title)** folder and the \(state.selectedPeerIds.count) chats selected below."
|
||||
peersHeaderString = "\(state.selectedPeerIds.count) CHATS SELECTED"
|
||||
chatCountString = presentationData.strings.FolderLinkScreen_TitleDescriptionSelected(title, presentationData.strings.FolderLinkScreen_TitleDescriptionSelectedCount(Int32(state.selectedPeerIds.count))).string
|
||||
peersHeaderString = presentationData.strings.FolderLinkScreen_ChatsSectionHeaderSelected(Int32(state.selectedPeerIds.count))
|
||||
if allPeers.count > 1 {
|
||||
selectAllString = allSelected ? "DESELECT ALL" : "SELECT ALL"
|
||||
selectAllString = allSelected ? presentationData.strings.FolderLinkScreen_ChatsSectionHeaderActionDeselectAll : presentationData.strings.FolderLinkScreen_ChatsSectionHeaderActionSelectAll
|
||||
}
|
||||
}
|
||||
entries.append(.header(chatCountString))
|
||||
|
||||
//TODO:localize
|
||||
|
||||
if canShareChats {
|
||||
entries.append(.mainLinkHeader("INVITE LINK"))
|
||||
entries.append(.mainLinkHeader(presentationData.strings.FolderLinkScreen_LinkSectionHeader))
|
||||
entries.append(.mainLink(link: state.currentLink, isGenerating: state.generatingLink))
|
||||
}
|
||||
|
||||
@ -290,12 +279,12 @@ private func folderInviteLinkListControllerEntries(
|
||||
if !canShareLinkToPeer(peer: peer) {
|
||||
if case let .user(user) = peer {
|
||||
if user.botInfo != nil {
|
||||
disabledReasonText = "you can't share chats with bots"
|
||||
disabledReasonText = presentationData.strings.FolderLinkScreen_LabelUnavailableBot
|
||||
} else {
|
||||
disabledReasonText = "you can't share private chats"
|
||||
disabledReasonText = presentationData.strings.FolderLinkScreen_LabelUnavailableUser
|
||||
}
|
||||
} else {
|
||||
disabledReasonText = "you can't invite others here"
|
||||
disabledReasonText = presentationData.strings.FolderLinkScreen_LabelUnavailableGeneric
|
||||
}
|
||||
}
|
||||
entries.append(.peer(index: entries.count, peer: peer, isSelected: state.selectedPeerIds.contains(peer.id), disabledReasonText: disabledReasonText))
|
||||
@ -420,15 +409,14 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
//TODO:localize
|
||||
items.append(.action(ContextMenuActionItem(text: "Name Link", icon: { theme in
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.FolderLinkScreen_ContextActionNameLink, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pencil"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { _, f in
|
||||
f(.dismissWithoutContent)
|
||||
|
||||
let state = stateValue.with({ $0 })
|
||||
|
||||
let promptController = promptController(sharedContext: context.sharedContext, updatedPresentationData: updatedPresentationData, text: "Name This Link", titleFont: .bold, value: state.title ?? "", characterLimit: 32, apply: { value in
|
||||
let promptController = promptController(sharedContext: context.sharedContext, updatedPresentationData: updatedPresentationData, text: presentationData.strings.FolderLinkScreen_NameLink_Title, titleFont: .bold, value: state.title ?? "", characterLimit: 32, apply: { value in
|
||||
if let value {
|
||||
updateState { state in
|
||||
var state = state
|
||||
@ -481,6 +469,8 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
|
||||
let contextController = ContextController(account: context.account, presentationData: presentationData, source: .reference(InviteLinkContextReferenceContentSource(controller: controller, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
|
||||
presentInGlobalOverlayImpl?(contextController)
|
||||
}, peerAction: { peer, isEnabled in
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
if isEnabled {
|
||||
var added = false
|
||||
updateState { state in
|
||||
@ -503,17 +493,15 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
|
||||
didDisplayAddPeerNotice = true
|
||||
|
||||
dismissTooltipsImpl?()
|
||||
//TODO:localize
|
||||
displayTooltipImpl?(.info(title: nil, text: "People who already used the invite link will be able to join newly added chats.", timeout: 8), true)
|
||||
displayTooltipImpl?(.info(title: nil, text: presentationData.strings.FolderLinkScreen_ToastNewChatAdded, timeout: 8), true)
|
||||
}
|
||||
} else {
|
||||
//TODO:localize
|
||||
let text: String
|
||||
if case let .user(user) = peer {
|
||||
if user.botInfo != nil {
|
||||
text = "You can't share chats with bots"
|
||||
text = presentationData.strings.FolderLinkScreen_AlertTextUnavailableBot
|
||||
} else {
|
||||
text = "You can't share private chats"
|
||||
text = presentationData.strings.FolderLinkScreen_AlertTextUnavailableUser
|
||||
}
|
||||
} else {
|
||||
var isGroup = true
|
||||
@ -523,15 +511,15 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
|
||||
}
|
||||
if isGroup {
|
||||
if isPrivate {
|
||||
text = "You don't have the admin rights to share invite links to this private group."
|
||||
text = presentationData.strings.FolderLinkScreen_AlertTextUnavailablePrivateGroup
|
||||
} else {
|
||||
text = "You don't have the admin rights to share invite links to this group chat."
|
||||
text = presentationData.strings.FolderLinkScreen_AlertTextUnavailablePublicGroup
|
||||
}
|
||||
} else {
|
||||
if isPrivate {
|
||||
text = "You don't have the admin rights to share invite links to this private channel."
|
||||
text = presentationData.strings.FolderLinkScreen_AlertTextUnavailablePrivateChannel
|
||||
} else {
|
||||
text = "You don't have the admin rights to share invite links to this channel."
|
||||
text = presentationData.strings.FolderLinkScreen_AlertTextUnavailablePublicChannel
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -605,12 +593,11 @@ 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.", timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.FolderLinkScreen_SaveUnknownError, timeout: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
||||
}, completed: {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
linkUpdated(ExportedChatFolderLink(title: state.title ?? "", link: currentLink.link, peerIds: Array(state.selectedPeerIds), isRevoked: false))
|
||||
//TODO:localize
|
||||
displayTooltipImpl?(.info(title: nil, text: "Link updated", timeout: 3), false)
|
||||
displayTooltipImpl?(.info(title: nil, text: presentationData.strings.FolderLinkScreen_ToastLinkUpdated, timeout: 3), false)
|
||||
|
||||
dismissImpl?()
|
||||
}))
|
||||
@ -666,10 +653,9 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
|
||||
animateChanges = true
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
let title: ItemListControllerTitle
|
||||
|
||||
var folderTitle = "Share Folder"
|
||||
var folderTitle = presentationData.strings.FolderLinkScreen_Title
|
||||
if let title = state.title, !title.isEmpty {
|
||||
folderTitle = title
|
||||
}
|
||||
@ -742,13 +728,12 @@ public func folderInviteLinkListController(context: AccountContext, updatedPrese
|
||||
|
||||
if hasChanges {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
//TODO:localize
|
||||
presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: "Unsaved Changes", text: "You have changed the settings of this folder. Apply changes?", actions: [
|
||||
TextAlertAction(type: .genericAction, title: "Discard", action: {
|
||||
presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.FolderLinkScreen_SaveAlertTitle, text: presentationData.strings.FolderLinkScreen_SaveAlertText, actions: [
|
||||
TextAlertAction(type: .genericAction, title: presentationData.strings.FolderLinkScreen_SaveAlertActionDiscard, action: {
|
||||
f()
|
||||
dismissImpl?()
|
||||
}),
|
||||
TextAlertAction(type: .defaultAction, title: state.selectedPeerIds.isEmpty ? "Continue" : "Apply", action: {
|
||||
TextAlertAction(type: .defaultAction, title: state.selectedPeerIds.isEmpty ? presentationData.strings.FolderLinkScreen_SaveAlertActionApply : presentationData.strings.FolderLinkScreen_SaveAlertActionContinue, action: {
|
||||
applyChangesImpl?()
|
||||
})
|
||||
]), nil)
|
||||
|
@ -227,8 +227,7 @@ private enum InviteLinkViewEntry: Comparable, Identifiable {
|
||||
case let .importer(_, _, dateTimeFormat, peer, date, joinedViaFolderLink, loading):
|
||||
let dateString: String
|
||||
if joinedViaFolderLink {
|
||||
//TODO:localize
|
||||
dateString = "joined via a folder invite link"
|
||||
dateString = presentationData.strings.InviteLink_LabelJoinedViaFolder
|
||||
} else {
|
||||
dateString = stringForFullDate(timestamp: date, strings: presentationData.strings, dateTimeFormat: dateTimeFormat)
|
||||
}
|
||||
|
@ -308,12 +308,7 @@ public class ItemListFolderInviteLinkListItemNode: ItemListRevealOptionsItemNode
|
||||
titleText = invite.title
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
if invite.peerIds.count == 1 {
|
||||
subtitleText = "includes 1 chat"
|
||||
} else {
|
||||
subtitleText = "includes \(invite.peerIds.count) chats"
|
||||
}
|
||||
subtitleText = item.presentationData.strings.ChatListFilter_LinkLabelChatCount(Int32(invite.peerIds.count))
|
||||
} else {
|
||||
titleText = " "
|
||||
subtitleText = " "
|
||||
@ -519,8 +514,7 @@ public class ItemListFolderInviteLinkListItemNode: ItemListRevealOptionsItemNode
|
||||
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)]))
|
||||
strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.presentationData.strings.ChatListFilter_LinkActionDelete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)]))
|
||||
} else {
|
||||
strongSelf.setRevealOptions((left: [], right: []))
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
@class PGCameraMovieWriter;
|
||||
@class PGRectangleDetector;
|
||||
@class SQueue;
|
||||
|
||||
@interface PGCameraCaptureSession : AVCaptureSession
|
||||
|
||||
@ -65,4 +66,6 @@
|
||||
|
||||
+ (bool)_isZoomAvailableForDevice:(AVCaptureDevice *)device;
|
||||
|
||||
+ (SQueue *)cameraQueue;
|
||||
|
||||
@end
|
||||
|
@ -49,6 +49,6 @@
|
||||
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context items:(NSArray *)items focusItem:(id<TGModernGalleryItem>)focusItem selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext hasCaptions:(bool)hasCaptions allowCaptionEntities:(bool)allowCaptionEntities hasTimer:(bool)hasTimer onlyCrop:(bool)onlyCrop inhibitDocumentCaptions:(bool)inhibitDocumentCaptions hasSelectionPanel:(bool)hasSelectionPanel hasCamera:(bool)hasCamera recipientName:(NSString *)recipientName;
|
||||
|
||||
- (void)presentPhotoEditorForItem:(id<TGModernGalleryEditableItem>)item tab:(TGPhotoEditorTab)tab;
|
||||
- (void)presentPhotoEditorForItem:(id<TGModernGalleryEditableItem>)item tab:(TGPhotoEditorTab)tab snapshots:(NSArray *)snapshots;
|
||||
- (void)presentPhotoEditorForItem:(id<TGModernGalleryEditableItem>)item tab:(TGPhotoEditorTab)tab snapshots:(NSArray *)snapshots fromRect:(CGRect)fromRect;
|
||||
|
||||
@end
|
||||
|
@ -48,6 +48,8 @@ typedef NS_ENUM(NSUInteger, TGModernGalleryScrollAnimationDirection) {
|
||||
- (void)dismissWhenReady;
|
||||
- (void)dismissWhenReadyAnimated:(bool)animated;
|
||||
|
||||
- (void)setScrollViewHidden:(bool)hidden;
|
||||
|
||||
- (bool)isFullyOpaque;
|
||||
|
||||
@end
|
||||
|
@ -23,7 +23,8 @@ typedef enum {
|
||||
TGPhotoEditorControllerVideoIntent = (1 << 4),
|
||||
TGPhotoEditorControllerForumAvatarIntent = (1 << 5),
|
||||
TGPhotoEditorControllerSuggestedAvatarIntent = (1 << 6),
|
||||
TGPhotoEditorControllerSuggestingAvatarIntent = (1 << 7)
|
||||
TGPhotoEditorControllerSuggestingAvatarIntent = (1 << 7),
|
||||
TGPhotoEditorControllerWallpaperIntent = (1 << 8)
|
||||
} TGPhotoEditorControllerIntent;
|
||||
|
||||
@interface TGPhotoEditorController : TGOverlayController
|
||||
@ -67,6 +68,8 @@ typedef enum {
|
||||
|
||||
@property (nonatomic, strong) UIView<TGPhotoDrawingEntitiesView> *entitiesView;
|
||||
|
||||
@property (nonatomic, assign) bool ignoreCropForResult;
|
||||
|
||||
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context item:(id<TGMediaEditableItem>)item intent:(TGPhotoEditorControllerIntent)intent adjustments:(id<TGMediaEditAdjustments>)adjustments caption:(NSAttributedString *)caption screenImage:(UIImage *)screenImage availableTabs:(TGPhotoEditorTab)availableTabs selectedTab:(TGPhotoEditorTab)selectedTab;
|
||||
|
||||
- (void)dismissEditor;
|
||||
|
@ -46,6 +46,7 @@ CGSize TGPhotoThumbnailSizeForCurrentScreen();
|
||||
CGSize TGPhotoEditorScreenImageMaxSize();
|
||||
|
||||
extern const CGSize TGPhotoEditorResultImageMaxSize;
|
||||
extern const CGSize TGPhotoEditorResultImageWallpaperMaxSize;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
@ -4,6 +4,8 @@
|
||||
|
||||
+ (void)presentWithContext:(id<LegacyComponentsContext>)context parentController:(TGViewController *)parentController image:(UIImage *)image video:(NSURL *)video stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext transitionView:(UIView *)transitionView senderName:(NSString *)senderName didFinishWithImage:(void (^)(UIImage *image))didFinishWithImage didFinishWithVideo:(void (^)(UIImage *image, NSURL *url, TGVideoEditAdjustments *adjustments))didFinishWithVideo dismissed:(void (^)(void))dismissed;
|
||||
|
||||
+ (void)presentWithContext:(id<LegacyComponentsContext>)context controller:(TGViewController *)controller caption:(NSAttributedString *)caption withItem:(id<TGMediaEditableItem, TGMediaSelectableItem>)item paint:(bool)paint recipientName:(NSString *)recipientName stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext snapshots:(NSArray *)snapshots immediate:(bool)immediate appeared:(void (^)(void))appeared completion:(void (^)(id<TGMediaEditableItem>, TGMediaEditingContext *))completion dismissed:(void (^)())dismissed;
|
||||
+ (void)presentWithContext:(id<LegacyComponentsContext>)context controller:(TGViewController *)controller caption:(NSAttributedString *)caption withItem:(id<TGMediaEditableItem, TGMediaSelectableItem>)item paint:(bool)paint adjustments:(bool)adjustments recipientName:(NSString *)recipientName stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext fromRect:(CGRect)fromRect mainSnapshot:(UIView *)mainSnapshot snapshots:(NSArray *)snapshots immediate:(bool)immediate appeared:(void (^)(void))appeared completion:(void (^)(id<TGMediaEditableItem>, TGMediaEditingContext *))completion dismissed:(void (^)())dismissed;
|
||||
|
||||
+ (void)presentEditorWithContext:(id<LegacyComponentsContext>)context controller:(TGViewController *)controller withItem:(id<TGMediaEditableItem>)item cropRect:(CGRect)cropRect adjustments:(id<TGMediaEditAdjustments>)adjustments referenceView:(UIView *)referenceView completion:(void (^)(UIImage *, id<TGMediaEditAdjustments>))completion fullSizeCompletion:(void (^)(UIImage *))fullSizeCompletion beginTransitionOut:(void (^)())beginTransitionOut finishTransitionOut:(void (^)())finishTransitionOut;
|
||||
|
||||
@end
|
||||
|
@ -218,6 +218,11 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
- (bool)isRegional
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
- (bool)isAvialableForVideo
|
||||
{
|
||||
return false;
|
||||
|
@ -120,7 +120,7 @@ NSString *const PGCameraAdjustingFocusKey = @"adjustingFocus";
|
||||
TGLegacyLog(@"ERROR: Camera runtime error: %@", notification.userInfo[AVCaptureSessionErrorKey]);
|
||||
|
||||
__weak PGCamera *weakSelf = self;
|
||||
TGDispatchAfter(1.5f, [PGCamera cameraQueue]._dispatch_queue, ^
|
||||
TGDispatchAfter(1.5f, [PGCameraCaptureSession cameraQueue]._dispatch_queue, ^
|
||||
{
|
||||
__strong PGCamera *strongSelf = weakSelf;
|
||||
if (strongSelf == nil || strongSelf->_invalidated)
|
||||
@ -198,7 +198,7 @@ NSString *const PGCameraAdjustingFocusKey = @"adjustingFocus";
|
||||
[previewView setupWithCamera:self];
|
||||
|
||||
__weak PGCamera *weakSelf = self;
|
||||
[[PGCamera cameraQueue] dispatch:^
|
||||
[[PGCameraCaptureSession cameraQueue] dispatch:^
|
||||
{
|
||||
__strong PGCamera *strongSelf = weakSelf;
|
||||
if (strongSelf == nil || strongSelf->_invalidated)
|
||||
@ -225,7 +225,7 @@ NSString *const PGCameraAdjustingFocusKey = @"adjustingFocus";
|
||||
if (_invalidated)
|
||||
return;
|
||||
|
||||
[[PGCamera cameraQueue] dispatch:^
|
||||
[[PGCameraCaptureSession cameraQueue] dispatch:^
|
||||
{
|
||||
if (self.captureSession.isRunning)
|
||||
return;
|
||||
@ -261,10 +261,11 @@ NSString *const PGCameraAdjustingFocusKey = @"adjustingFocus";
|
||||
|
||||
TGLegacyLog(@"Camera: stop capture");
|
||||
|
||||
[[PGCamera cameraQueue] dispatch:^
|
||||
[[PGCameraCaptureSession cameraQueue] dispatch:^
|
||||
{
|
||||
if (_invalidated)
|
||||
{
|
||||
#if !TARGET_IPHONE_SIMULATOR
|
||||
[self.captureSession beginConfiguration];
|
||||
|
||||
[self.captureSession resetFlashMode];
|
||||
@ -279,16 +280,21 @@ NSString *const PGCameraAdjustingFocusKey = @"adjustingFocus";
|
||||
for (AVCaptureOutput *output in self.captureSession.outputs)
|
||||
[self.captureSession removeOutput:output];
|
||||
|
||||
#if !TARGET_IPHONE_SIMULATOR
|
||||
[self.captureSession commitConfiguration];
|
||||
#endif
|
||||
}
|
||||
|
||||
TGLegacyLog(@"Camera: stop running");
|
||||
#if !TARGET_IPHONE_SIMULATOR
|
||||
[self.captureSession stopRunning];
|
||||
@try {
|
||||
[self.captureSession stopRunning];
|
||||
} @catch (NSException *exception) {
|
||||
TGLegacyLog(@"Camera: caught exception – %@", exception.description);
|
||||
[self.captureSession commitConfiguration];
|
||||
[self.captureSession stopRunning];
|
||||
TGLegacyLog(@"Camera: seems to be successfully resolved");
|
||||
}
|
||||
#endif
|
||||
|
||||
_capturing = false;
|
||||
|
||||
TGDispatchOnMainThread(^
|
||||
@ -328,9 +334,9 @@ NSString *const PGCameraAdjustingFocusKey = @"adjustingFocus";
|
||||
};
|
||||
|
||||
if (synchronous)
|
||||
[[PGCamera cameraQueue] dispatchSync:block];
|
||||
[[PGCameraCaptureSession cameraQueue] dispatchSync:block];
|
||||
else
|
||||
[[PGCamera cameraQueue] dispatch:block];
|
||||
[[PGCameraCaptureSession cameraQueue] dispatch:block];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
@ -361,7 +367,7 @@ NSString *const PGCameraAdjustingFocusKey = @"adjustingFocus";
|
||||
{
|
||||
bool videoMirrored = !self.disableResultMirroring ? _previewView.captureConnection.videoMirrored : false;
|
||||
|
||||
[[PGCamera cameraQueue] dispatch:^
|
||||
[[PGCameraCaptureSession cameraQueue] dispatch:^
|
||||
{
|
||||
if (!self.captureSession.isRunning || self.captureSession.imageOutput.isCapturingStillImage || _invalidated)
|
||||
return;
|
||||
@ -410,13 +416,13 @@ NSString *const PGCameraAdjustingFocusKey = @"adjustingFocus";
|
||||
if (CFAbsoluteTimeGetCurrent() - _captureStartTime > 0.4)
|
||||
takePhoto();
|
||||
else
|
||||
TGDispatchAfter(0.4 - delta, [[PGCamera cameraQueue] _dispatch_queue], takePhoto);
|
||||
TGDispatchAfter(0.4 - delta, [[PGCameraCaptureSession cameraQueue] _dispatch_queue], takePhoto);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)startVideoRecordingForMoment:(bool)moment completion:(void (^)(NSURL *, CGAffineTransform transform, CGSize dimensions, NSTimeInterval duration, bool success))completion
|
||||
{
|
||||
[[PGCamera cameraQueue] dispatch:^
|
||||
[[PGCameraCaptureSession cameraQueue] dispatch:^
|
||||
{
|
||||
if (!self.captureSession.isRunning || _invalidated)
|
||||
return;
|
||||
@ -443,7 +449,7 @@ NSString *const PGCameraAdjustingFocusKey = @"adjustingFocus";
|
||||
if (CFAbsoluteTimeGetCurrent() - _captureStartTime > 1.5)
|
||||
startRecording();
|
||||
else
|
||||
TGDispatchAfter(1.5, [[PGCamera cameraQueue] _dispatch_queue], startRecording);
|
||||
TGDispatchAfter(1.5, [[PGCameraCaptureSession cameraQueue] _dispatch_queue], startRecording);
|
||||
|
||||
TGDispatchOnMainThread(^
|
||||
{
|
||||
@ -455,7 +461,7 @@ NSString *const PGCameraAdjustingFocusKey = @"adjustingFocus";
|
||||
|
||||
- (void)stopVideoRecording
|
||||
{
|
||||
[[PGCamera cameraQueue] dispatch:^
|
||||
[[PGCameraCaptureSession cameraQueue] dispatch:^
|
||||
{
|
||||
[self.captureSession stopVideoRecording];
|
||||
|
||||
@ -496,7 +502,7 @@ NSString *const PGCameraAdjustingFocusKey = @"adjustingFocus";
|
||||
if (strongSelf == nil)
|
||||
return;
|
||||
|
||||
[[PGCamera cameraQueue] dispatch:^
|
||||
[[PGCameraCaptureSession cameraQueue] dispatch:^
|
||||
{
|
||||
strongSelf.captureSession.currentMode = cameraMode;
|
||||
|
||||
@ -584,7 +590,7 @@ NSString *const PGCameraAdjustingFocusKey = @"adjustingFocus";
|
||||
|
||||
- (void)_setFocusPoint:(CGPoint)point focusMode:(AVCaptureFocusMode)focusMode exposureMode:(AVCaptureExposureMode)exposureMode monitorSubjectAreaChange:(bool)monitorSubjectAreaChange
|
||||
{
|
||||
[[PGCamera cameraQueue] dispatch:^
|
||||
[[PGCameraCaptureSession cameraQueue] dispatch:^
|
||||
{
|
||||
if (self.disabled)
|
||||
return;
|
||||
@ -600,7 +606,7 @@ NSString *const PGCameraAdjustingFocusKey = @"adjustingFocus";
|
||||
|
||||
- (void)beginExposureTargetBiasChange
|
||||
{
|
||||
[[PGCamera cameraQueue] dispatch:^
|
||||
[[PGCameraCaptureSession cameraQueue] dispatch:^
|
||||
{
|
||||
if (self.disabled)
|
||||
return;
|
||||
@ -611,7 +617,7 @@ NSString *const PGCameraAdjustingFocusKey = @"adjustingFocus";
|
||||
|
||||
- (void)setExposureTargetBias:(CGFloat)bias
|
||||
{
|
||||
[[PGCamera cameraQueue] dispatch:^
|
||||
[[PGCameraCaptureSession cameraQueue] dispatch:^
|
||||
{
|
||||
if (self.disabled)
|
||||
return;
|
||||
@ -622,7 +628,7 @@ NSString *const PGCameraAdjustingFocusKey = @"adjustingFocus";
|
||||
|
||||
- (void)endExposureTargetBiasChange
|
||||
{
|
||||
[[PGCamera cameraQueue] dispatch:^
|
||||
[[PGCameraCaptureSession cameraQueue] dispatch:^
|
||||
{
|
||||
if (self.disabled)
|
||||
return;
|
||||
@ -661,7 +667,7 @@ NSString *const PGCameraAdjustingFocusKey = @"adjustingFocus";
|
||||
|
||||
- (void)setFlashMode:(PGCameraFlashMode)flashMode
|
||||
{
|
||||
[[PGCamera cameraQueue] dispatch:^
|
||||
[[PGCameraCaptureSession cameraQueue] dispatch:^
|
||||
{
|
||||
self.captureSession.currentFlashMode = flashMode;
|
||||
}];
|
||||
@ -689,7 +695,7 @@ NSString *const PGCameraAdjustingFocusKey = @"adjustingFocus";
|
||||
if (strongSelf == nil)
|
||||
return;
|
||||
|
||||
[[PGCamera cameraQueue] dispatch:^
|
||||
[[PGCameraCaptureSession cameraQueue] dispatch:^
|
||||
{
|
||||
[strongSelf.captureSession setCurrentCameraPosition:targetCameraPosition];
|
||||
|
||||
@ -744,7 +750,7 @@ NSString *const PGCameraAdjustingFocusKey = @"adjustingFocus";
|
||||
if (self.cameraMode == PGCameraModeVideo) {
|
||||
animated = false;
|
||||
}
|
||||
[[PGCamera cameraQueue] dispatch:^
|
||||
[[PGCameraCaptureSession cameraQueue] dispatch:^
|
||||
{
|
||||
if (self.disabled)
|
||||
return;
|
||||
@ -786,18 +792,6 @@ NSString *const PGCameraAdjustingFocusKey = @"adjustingFocus";
|
||||
return ([PGCameraCaptureSession _deviceWithCameraPosition:PGCameraPositionFront] != nil);
|
||||
}
|
||||
|
||||
+ (SQueue *)cameraQueue
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
static SQueue *queue = nil;
|
||||
dispatch_once(&onceToken, ^
|
||||
{
|
||||
queue = [[SQueue alloc] init];
|
||||
});
|
||||
|
||||
return queue;
|
||||
}
|
||||
|
||||
+ (AVCaptureVideoOrientation)_videoOrientationForInterfaceOrientation:(UIInterfaceOrientation)deviceOrientation mirrored:(bool)mirrored
|
||||
{
|
||||
switch (deviceOrientation)
|
||||
|
@ -208,6 +208,7 @@ const NSInteger PGCameraFrameRate = 30;
|
||||
|
||||
- (void)reset
|
||||
{
|
||||
NSAssert([[PGCameraCaptureSession cameraQueue] isCurrentQueue], @"[[PGCameraCaptureSession cameraQueue] isCurrentQueue]");
|
||||
[self beginConfiguration];
|
||||
|
||||
[self _removeAudioInputEndAudioSession:true];
|
||||
@ -259,6 +260,8 @@ const NSInteger PGCameraFrameRate = 30;
|
||||
|
||||
- (void)setCurrentMode:(PGCameraMode)mode
|
||||
{
|
||||
NSAssert([[PGCameraCaptureSession cameraQueue] isCurrentQueue], @"[[PGCameraCaptureSession cameraQueue] isCurrentQueue]");
|
||||
|
||||
_currentMode = mode;
|
||||
|
||||
[self beginConfiguration];
|
||||
@ -804,6 +807,7 @@ const NSInteger PGCameraFrameRate = 30;
|
||||
|
||||
- (void)setCurrentCameraPosition:(PGCameraPosition)position
|
||||
{
|
||||
NSAssert([[PGCameraCaptureSession cameraQueue] isCurrentQueue], @"[[PGCameraCaptureSession cameraQueue] isCurrentQueue]");
|
||||
AVCaptureDevice *deviceForTargetPosition = [PGCameraCaptureSession _deviceWithCameraPosition:position];
|
||||
if ([_videoDevice isEqual:deviceForTargetPosition])
|
||||
return;
|
||||
@ -1123,4 +1127,18 @@ static UIImageOrientation TGSnapshotOrientationForVideoOrientation(bool mirrored
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
+ (SQueue *)cameraQueue
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
static SQueue *queue = nil;
|
||||
dispatch_once(&onceToken, ^
|
||||
{
|
||||
queue = [[SQueue alloc] init];
|
||||
});
|
||||
|
||||
return queue;
|
||||
}
|
||||
|
||||
@end
|
||||
|
@ -35,6 +35,7 @@ typedef enum
|
||||
@property (nonatomic, readonly) NSInteger order;
|
||||
|
||||
@property (nonatomic, readonly) bool isHidden;
|
||||
@property (nonatomic, readonly) bool isRegional;
|
||||
|
||||
@property (nonatomic, readonly) NSString *shaderString;
|
||||
@property (nonatomic, readonly) NSString *ancillaryShaderString;
|
||||
|
@ -38,6 +38,11 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
- (bool)isRegional
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
- (bool)isAvialableForVideo
|
||||
{
|
||||
return true;
|
||||
|
@ -38,6 +38,11 @@
|
||||
return (ABS(((NSNumber *)self.displayValue).floatValue - (float)self.defaultValue) < FLT_EPSILON);
|
||||
}
|
||||
|
||||
- (bool)isRegional
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
- (NSArray *)parameters
|
||||
{
|
||||
if (!_parameters)
|
||||
|
@ -81,9 +81,6 @@
|
||||
[[NSFileManager defaultManager] removeItemAtPath:[self filePath] error:nil];
|
||||
}
|
||||
|
||||
#define PGTick NSDate *startTime = [NSDate date]
|
||||
#define PGTock NSLog(@"!=========== %s Time: %f", __func__, -[startTime timeIntervalSinceNow])
|
||||
|
||||
- (void)_saveToDisk:(UIImage *)image
|
||||
{
|
||||
if (image == nil)
|
||||
|
@ -338,10 +338,10 @@
|
||||
|
||||
- (void)presentPhotoEditorForItem:(id<TGModernGalleryEditableItem>)item tab:(TGPhotoEditorTab)tab
|
||||
{
|
||||
[self presentPhotoEditorForItem:item tab:tab snapshots:@[]];
|
||||
[self presentPhotoEditorForItem:item tab:tab snapshots:@[] fromRect:CGRectZero];
|
||||
}
|
||||
|
||||
- (void)presentPhotoEditorForItem:(id<TGModernGalleryEditableItem>)item tab:(TGPhotoEditorTab)tab snapshots:(NSArray *)snapshots
|
||||
- (void)presentPhotoEditorForItem:(id<TGModernGalleryEditableItem>)item tab:(TGPhotoEditorTab)tab snapshots:(NSArray *)snapshots fromRect:(CGRect)fromRect
|
||||
{
|
||||
__weak TGMediaPickerGalleryModel *weakSelf = self;
|
||||
|
||||
@ -356,12 +356,15 @@
|
||||
|
||||
CGRect refFrame = CGRectZero;
|
||||
UIView *editorReferenceView = [self referenceViewForItem:item frame:&refFrame];
|
||||
if (!CGRectEqualToRect(fromRect, CGRectZero)) {
|
||||
refFrame = fromRect;
|
||||
}
|
||||
UIView *referenceView = nil;
|
||||
UIImage *screenImage = nil;
|
||||
UIView *referenceParentView = nil;
|
||||
UIImage *image = nil;
|
||||
|
||||
UIView<TGPhotoDrawingEntitiesView> *entitiesView = nil;
|
||||
UIView<TGPhotoDrawingEntitiesView> *entitiesView = nil;
|
||||
|
||||
id<TGMediaEditableItem> editableMediaItem = item.editableMediaItem;
|
||||
|
||||
|
@ -166,6 +166,12 @@ static void adjustFrameRate(CAAnimation *animation) {
|
||||
[self dismissWhenReadyAnimated:animated force:false];
|
||||
}
|
||||
|
||||
- (void)setScrollViewHidden:(bool)hidden {
|
||||
TGDispatchAfter(0.01, dispatch_get_main_queue(), ^{
|
||||
_view.scrollView.hidden = hidden;
|
||||
});
|
||||
}
|
||||
|
||||
- (void)dismissWhenReadyAnimated:(bool)animated force:(bool)force
|
||||
{
|
||||
if (animated) {
|
||||
|
@ -982,14 +982,23 @@
|
||||
bool hasImageAdjustments = editorValues.toolsApplied || saveOnly;
|
||||
bool hasPainting = editorValues.hasPainting;
|
||||
bool hasAnimation = editorValues.paintingData.hasAnimation;
|
||||
bool ignoreCropForResult = self.ignoreCropForResult;
|
||||
|
||||
SSignal *(^imageCropSignal)(UIImage *, bool) = ^(UIImage *image, bool resize)
|
||||
{
|
||||
return [[SSignal alloc] initWithGenerator:^id<SDisposable>(SSubscriber *subscriber)
|
||||
{
|
||||
UIImage *paintingImage = !hasImageAdjustments ? editorValues.paintingData.image : nil;
|
||||
UIImage *croppedImage = TGPhotoEditorCrop(image, paintingImage, photoEditor.cropOrientation, photoEditor.cropRotation, photoEditor.cropRect, photoEditor.cropMirrored, TGPhotoEditorResultImageMaxSize, photoEditor.originalSize, resize);
|
||||
[subscriber putNext:croppedImage];
|
||||
if (ignoreCropForResult) {
|
||||
if (image.size.width > TGPhotoEditorResultImageWallpaperMaxSize.width || image.size.height > TGPhotoEditorResultImageWallpaperMaxSize.width) {
|
||||
[subscriber putNext:TGPhotoEditorFitImage(image, TGPhotoEditorResultImageWallpaperMaxSize)];
|
||||
} else {
|
||||
[subscriber putNext:image];
|
||||
}
|
||||
} else {
|
||||
UIImage *paintingImage = !hasImageAdjustments ? editorValues.paintingData.image : nil;
|
||||
UIImage *croppedImage = TGPhotoEditorCrop(image, paintingImage, photoEditor.cropOrientation, photoEditor.cropRotation, photoEditor.cropRect, photoEditor.cropMirrored, TGPhotoEditorResultImageMaxSize, photoEditor.originalSize, resize);
|
||||
[subscriber putNext:croppedImage];
|
||||
}
|
||||
[subscriber putCompletion];
|
||||
|
||||
return nil;
|
||||
@ -1045,12 +1054,18 @@
|
||||
void (^didFinishRenderingFullSizeImage)(UIImage *) = self.didFinishRenderingFullSizeImage;
|
||||
void (^didFinishEditing)(id<TGMediaEditAdjustments>, UIImage *, UIImage *, bool , void(^)(void)) = self.didFinishEditing;
|
||||
|
||||
TGPhotoEditorControllerIntent intent = _intent;
|
||||
[[[[renderedImageSignal map:^id(UIImage *image)
|
||||
{
|
||||
if (!hasImageAdjustments)
|
||||
{
|
||||
if (hasPainting && !hasAnimation && didFinishRenderingFullSizeImage != nil)
|
||||
didFinishRenderingFullSizeImage(image);
|
||||
if (didFinishRenderingFullSizeImage != nil) {
|
||||
if (hasPainting && !hasAnimation) {
|
||||
didFinishRenderingFullSizeImage(image);
|
||||
} else if (intent == TGPhotoEditorControllerWallpaperIntent) {
|
||||
didFinishRenderingFullSizeImage(nil);
|
||||
}
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
@ -1155,6 +1170,13 @@
|
||||
_portraitToolbarView.alpha = 1.0f;
|
||||
_landscapeToolbarView.alpha = 1.0f;
|
||||
} completion:nil];
|
||||
|
||||
if (_intent == TGPhotoEditorControllerWallpaperIntent) {
|
||||
[UIView animateWithDuration:0.25f delay:0.15 options:UIViewAnimationOptionCurveLinear animations:^
|
||||
{
|
||||
_backgroundView.alpha = 1.0f;
|
||||
} completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)transitionOutSaving:(bool)saving completion:(void (^)(void))completion
|
||||
@ -1170,6 +1192,13 @@
|
||||
_landscapeToolbarView.alpha = 0.0f;
|
||||
}];
|
||||
|
||||
if (_intent == TGPhotoEditorControllerWallpaperIntent) {
|
||||
[UIView animateWithDuration:0.1f animations:^
|
||||
{
|
||||
_backgroundView.alpha = 0.0f;
|
||||
}];
|
||||
}
|
||||
|
||||
_currentTabController.beginTransitionOut = self.beginTransitionOut;
|
||||
[self setToolbarHidden:false animated:true];
|
||||
|
||||
|
@ -28,6 +28,9 @@ const CGFloat TGPhotoEditorToolbarSize = 49.0f;
|
||||
UIView *_transitionInReferenceView;
|
||||
UIView *_transitionInParentView;
|
||||
CGRect _transitionTargetFrame;
|
||||
|
||||
UIView *_upperTransitionView;
|
||||
CGRect _upperTransitionTargetFrame;
|
||||
}
|
||||
@end
|
||||
|
||||
@ -114,6 +117,9 @@ const CGFloat TGPhotoEditorToolbarSize = 49.0f;
|
||||
_dismissing = false;
|
||||
|
||||
CGRect targetFrame = [self _targetFrameForTransitionInFromFrame:referenceFrame];
|
||||
if (self.intent == TGPhotoEditorControllerWallpaperIntent) {
|
||||
targetFrame = [self.view convertRect:targetFrame toView: parentView];
|
||||
}
|
||||
|
||||
if (_CGRectEqualToRectWithEpsilon(targetFrame, referenceFrame, FLT_EPSILON))
|
||||
{
|
||||
@ -157,12 +163,20 @@ const CGFloat TGPhotoEditorToolbarSize = 49.0f;
|
||||
_transitionView = referenceView;
|
||||
}
|
||||
transitionViewSuperview = parentView;
|
||||
|
||||
if (self.intent == TGPhotoEditorControllerWallpaperIntent) {
|
||||
_upperTransitionView = [referenceView snapshotViewAfterScreenUpdates:false];
|
||||
_upperTransitionView.alpha = 0.0;
|
||||
_upperTransitionView.frame = [parentView convertRect:referenceFrame toView:self.view];
|
||||
_upperTransitionTargetFrame = [self _targetFrameForTransitionInFromFrame:referenceFrame];
|
||||
[self.view insertSubview:_upperTransitionView atIndex:0];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
_transitionView.hidden = false;
|
||||
_transitionView.frame = referenceFrame;
|
||||
_transitionTargetFrame = [self _targetFrameForTransitionInFromFrame:referenceFrame];
|
||||
_transitionTargetFrame = targetFrame;
|
||||
[transitionViewSuperview addSubview:_transitionView];
|
||||
}
|
||||
|
||||
@ -174,6 +188,9 @@ const CGFloat TGPhotoEditorToolbarSize = 49.0f;
|
||||
_transitionInProgress = true;
|
||||
|
||||
CGAffineTransform initialTransform = _transitionView.transform;
|
||||
[UIView animateWithDuration:0.25 animations:^{
|
||||
_upperTransitionView.alpha = 1.0;
|
||||
}];
|
||||
[UIView animateWithDuration:0.3f delay:0.0f options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionLayoutSubviews animations:^
|
||||
{
|
||||
if (_animateScale) {
|
||||
@ -182,6 +199,7 @@ const CGFloat TGPhotoEditorToolbarSize = 49.0f;
|
||||
_transitionView.transform = CGAffineTransformScale(initialTransform, scale, scale);
|
||||
} else {
|
||||
_transitionView.frame = _transitionTargetFrame;
|
||||
_upperTransitionView.frame = _upperTransitionTargetFrame;
|
||||
}
|
||||
} completion:^(BOOL finished) {
|
||||
_transitionInProgress = false;
|
||||
@ -201,6 +219,8 @@ const CGFloat TGPhotoEditorToolbarSize = 49.0f;
|
||||
}
|
||||
|
||||
[self _finishedTransitionInWithView:transitionView];
|
||||
[_upperTransitionView removeFromSuperview];
|
||||
_upperTransitionView = nil;
|
||||
}];
|
||||
}
|
||||
|
||||
@ -275,7 +295,11 @@ const CGFloat TGPhotoEditorToolbarSize = 49.0f;
|
||||
|
||||
if (saving)
|
||||
{
|
||||
[self _animatePreviewViewTransitionOutToFrame:CGRectNull saving:saving parentView:parentView completion:^
|
||||
CGRect targetFrame = CGRectNull;
|
||||
if (self.intent == TGPhotoEditorControllerWallpaperIntent) {
|
||||
targetFrame = referenceFrame;
|
||||
}
|
||||
[self _animatePreviewViewTransitionOutToFrame:targetFrame saving:saving parentView:parentView completion:^
|
||||
{
|
||||
if (completion != nil)
|
||||
completion();
|
||||
@ -316,6 +340,9 @@ const CGFloat TGPhotoEditorToolbarSize = 49.0f;
|
||||
orientation = UIInterfaceOrientationPortrait;
|
||||
|
||||
CGRect sourceFrame = [self transitionOutSourceFrameForReferenceFrame:referenceView.frame orientation:orientation];
|
||||
if (self.intent == TGPhotoEditorControllerWallpaperIntent) {
|
||||
sourceFrame = [self.view convertRect:sourceFrame toView: parentView];
|
||||
}
|
||||
CGRect targetFrame = referenceFrame;
|
||||
toTransitionView.frame = sourceFrame;
|
||||
|
||||
@ -334,7 +361,12 @@ const CGFloat TGPhotoEditorToolbarSize = 49.0f;
|
||||
};
|
||||
|
||||
[animations addObject:@1];
|
||||
[self _animatePreviewViewTransitionOutToFrame:targetFrame saving:saving parentView:nil completion:^
|
||||
|
||||
CGRect previewTargetFrame = targetFrame;
|
||||
if (self.intent == TGPhotoEditorControllerWallpaperIntent) {
|
||||
previewTargetFrame = [referenceView convertRect:referenceView.bounds toView:self.view];
|
||||
}
|
||||
[self _animatePreviewViewTransitionOutToFrame:previewTargetFrame saving:saving parentView:nil completion:^
|
||||
{
|
||||
onAnimationCompletion(@1);
|
||||
}];
|
||||
|
@ -7,6 +7,7 @@
|
||||
#import <Accelerate/Accelerate.h>
|
||||
|
||||
const CGSize TGPhotoEditorResultImageMaxSize = { 1280, 1280 };
|
||||
const CGSize TGPhotoEditorResultImageWallpaperMaxSize = { 2048, 2048 };
|
||||
const CGSize TGPhotoEditorScreenImageHardLimitSize = { 1280, 1280 };
|
||||
const CGSize TGPhotoEditorScreenImageHardLimitLegacySize = { 750, 750 };
|
||||
|
||||
|
@ -150,6 +150,9 @@ const CGFloat TGPhotoEditorToolsLandscapePanelSize = TGPhotoEditorToolsPanelSize
|
||||
}
|
||||
if (!tool.isHidden)
|
||||
{
|
||||
if (tool.isRegional && self.intent == TGPhotoEditorControllerWallpaperIntent) {
|
||||
continue;
|
||||
}
|
||||
[tools addObject:tool];
|
||||
if (tool.isSimple)
|
||||
[simpleTools addObject:tool];
|
||||
@ -451,24 +454,40 @@ const CGFloat TGPhotoEditorToolsLandscapePanelSize = TGPhotoEditorToolsPanelSize
|
||||
UIView *snapshotView = nil;
|
||||
POPSpringAnimation *snapshotAnimation = nil;
|
||||
|
||||
if (saving && CGRectIsNull(targetFrame) && parentView != nil)
|
||||
if (saving && parentView != nil)
|
||||
{
|
||||
snapshotView = [previewView snapshotViewAfterScreenUpdates:false];
|
||||
snapshotView.frame = previewView.frame;
|
||||
|
||||
CGSize fittedSize = TGScaleToSize(previewView.frame.size, self.view.frame.size);
|
||||
targetFrame = CGRectMake((self.view.frame.size.width - fittedSize.width) / 2, (self.view.frame.size.height - fittedSize.height) / 2, fittedSize.width, fittedSize.height);
|
||||
|
||||
[parentView addSubview:snapshotView];
|
||||
|
||||
snapshotAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewFrame];
|
||||
snapshotAnimation.fromValue = [NSValue valueWithCGRect:snapshotView.frame];
|
||||
snapshotAnimation.toValue = [NSValue valueWithCGRect:targetFrame];
|
||||
if (CGRectIsNull(targetFrame)) {
|
||||
snapshotView = [previewView snapshotViewAfterScreenUpdates:false];
|
||||
snapshotView.frame = previewView.frame;
|
||||
|
||||
CGSize fittedSize = TGScaleToSize(previewView.frame.size, self.view.frame.size);
|
||||
targetFrame = CGRectMake((self.view.frame.size.width - fittedSize.width) / 2, (self.view.frame.size.height - fittedSize.height) / 2, fittedSize.width, fittedSize.height);
|
||||
|
||||
[parentView addSubview:snapshotView];
|
||||
|
||||
snapshotAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewFrame];
|
||||
snapshotAnimation.fromValue = [NSValue valueWithCGRect:snapshotView.frame];
|
||||
snapshotAnimation.toValue = [NSValue valueWithCGRect:targetFrame];
|
||||
} else if (self.intent == TGPhotoEditorControllerWallpaperIntent) {
|
||||
snapshotView = [previewView snapshotViewAfterScreenUpdates:false];
|
||||
snapshotView.frame = [self.view convertRect:previewView.frame toView:parentView];
|
||||
|
||||
[parentView addSubview:snapshotView];
|
||||
|
||||
snapshotAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewFrame];
|
||||
snapshotAnimation.fromValue = [NSValue valueWithCGRect:snapshotView.frame];
|
||||
snapshotAnimation.toValue = [NSValue valueWithCGRect:targetFrame];
|
||||
}
|
||||
}
|
||||
|
||||
CGRect previewTargetFrame = targetFrame;
|
||||
if (self.intent == TGPhotoEditorControllerWallpaperIntent && saving) {
|
||||
previewTargetFrame = [parentView convertRect:targetFrame toView:self.view];
|
||||
}
|
||||
|
||||
POPSpringAnimation *previewAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewFrame];
|
||||
previewAnimation.fromValue = [NSValue valueWithCGRect:previewView.frame];
|
||||
previewAnimation.toValue = [NSValue valueWithCGRect:targetFrame];
|
||||
previewAnimation.toValue = [NSValue valueWithCGRect:previewTargetFrame];
|
||||
|
||||
POPSpringAnimation *previewAlphaAnimation = [TGPhotoEditorAnimation prepareTransitionAnimationForPropertyNamed:kPOPViewAlpha];
|
||||
previewAlphaAnimation.fromValue = @(previewView.alpha);
|
||||
@ -1020,6 +1039,8 @@ const CGFloat TGPhotoEditorToolsLandscapePanelSize = TGPhotoEditorToolsPanelSize
|
||||
{
|
||||
if (self.photoEditor.forVideo) {
|
||||
return TGPhotoEditorToolsTab | TGPhotoEditorTintTab | TGPhotoEditorCurvesTab;
|
||||
} else if (self.intent == TGPhotoEditorControllerWallpaperIntent) {
|
||||
return TGPhotoEditorToolsTab | TGPhotoEditorTintTab | TGPhotoEditorCurvesTab;
|
||||
} else {
|
||||
return TGPhotoEditorToolsTab | TGPhotoEditorTintTab | TGPhotoEditorBlurTab | TGPhotoEditorCurvesTab;
|
||||
}
|
||||
|
@ -154,7 +154,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
+ (void)presentWithContext:(id<LegacyComponentsContext>)context controller:(TGViewController *)controller caption:(NSAttributedString *)caption withItem:(id<TGMediaEditableItem, TGMediaSelectableItem>)item paint:(bool)paint recipientName:(NSString *)recipientName stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext snapshots:(NSArray *)snapshots immediate:(bool)immediate appeared:(void (^)(void))appeared completion:(void (^)(id<TGMediaEditableItem>, TGMediaEditingContext *))completion dismissed:(void (^)())dismissed
|
||||
+ (void)presentWithContext:(id<LegacyComponentsContext>)context controller:(TGViewController *)controller caption:(NSAttributedString *)caption withItem:(id<TGMediaEditableItem, TGMediaSelectableItem>)item paint:(bool)paint adjustments:(bool)adjustments recipientName:(NSString *)recipientName stickersContext:(id<TGPhotoPaintStickersContext>)stickersContext fromRect:(CGRect)fromRect mainSnapshot:(UIView *)mainSnapshot snapshots:(NSArray *)snapshots immediate:(bool)immediate appeared:(void (^)(void))appeared completion:(void (^)(id<TGMediaEditableItem>, TGMediaEditingContext *))completion dismissed:(void (^)())dismissed
|
||||
{
|
||||
id<LegacyComponentsOverlayWindowManager> windowManager = [context makeOverlayWindowManager];
|
||||
id<LegacyComponentsContext> windowContext = [windowManager context];
|
||||
@ -201,6 +201,11 @@
|
||||
[editingContext setCaption:caption forItem:editableItem];
|
||||
};
|
||||
|
||||
model.didFinishRenderingFullSizeImage = ^(id<TGMediaEditableItem> editableItem, UIImage *resultImage)
|
||||
{
|
||||
[editingContext setFullSizeImage:resultImage forItem:editableItem];
|
||||
};
|
||||
|
||||
model.interfaceView.hasSwipeGesture = false;
|
||||
galleryController.model = model;
|
||||
|
||||
@ -255,10 +260,10 @@
|
||||
}
|
||||
};
|
||||
|
||||
if (paint) {
|
||||
if (paint || adjustments) {
|
||||
[model.interfaceView immediateEditorTransitionIn];
|
||||
}
|
||||
|
||||
|
||||
for (UIView *view in snapshots) {
|
||||
[galleryController.view addSubview:view];
|
||||
}
|
||||
@ -269,9 +274,235 @@
|
||||
|
||||
if (paint) {
|
||||
TGDispatchAfter(0.05, dispatch_get_main_queue(), ^{
|
||||
[model presentPhotoEditorForItem:galleryItem tab:TGPhotoEditorPaintTab snapshots:snapshots];
|
||||
[model presentPhotoEditorForItem:galleryItem tab:TGPhotoEditorPaintTab snapshots:snapshots fromRect:fromRect];
|
||||
});
|
||||
} else if (adjustments) {
|
||||
TGDispatchAfter(0.05, dispatch_get_main_queue(), ^{
|
||||
[model presentPhotoEditorForItem:galleryItem tab:TGPhotoEditorToolsTab snapshots:snapshots fromRect:fromRect];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+ (void)presentEditorWithContext:(id<LegacyComponentsContext>)context controller:(TGViewController *)controller withItem:(id<TGMediaEditableItem>)item cropRect:(CGRect)cropRect adjustments:(id<TGMediaEditAdjustments>)adjustments referenceView:(UIView *)referenceView completion:(void (^)(UIImage *, id<TGMediaEditAdjustments>))completion fullSizeCompletion:(void (^)(UIImage *))fullSizeCompletion beginTransitionOut:(void (^)())beginTransitionOut finishTransitionOut:(void (^)())finishTransitionOut;
|
||||
{
|
||||
id<LegacyComponentsOverlayWindowManager> windowManager = [context makeOverlayWindowManager];
|
||||
|
||||
TGMediaEditingContext *editingContext = [[TGMediaEditingContext alloc] init];
|
||||
|
||||
UIImage *thumbnailImage;
|
||||
|
||||
NSDictionary *toolValues;
|
||||
if (adjustments != nil) {
|
||||
toolValues = adjustments.toolValues;
|
||||
} else {
|
||||
toolValues = @{};
|
||||
}
|
||||
PGPhotoEditorValues *editorValues = [PGPhotoEditorValues editorValuesWithOriginalSize:item.originalSize cropRect:cropRect cropRotation:0.0f cropOrientation:UIImageOrientationUp cropLockedAspectRatio:0.0 cropMirrored:false toolValues:toolValues paintingData:nil sendAsGif:false];
|
||||
|
||||
TGPhotoEditorController *editorController = [[TGPhotoEditorController alloc] initWithContext:[windowManager context] item:item intent:TGPhotoEditorControllerWallpaperIntent adjustments:editorValues caption:nil screenImage:thumbnailImage availableTabs:TGPhotoEditorToolsTab selectedTab:TGPhotoEditorToolsTab];
|
||||
editorController.editingContext = editingContext;
|
||||
editorController.dontHideStatusBar = true;
|
||||
editorController.ignoreCropForResult = true;
|
||||
|
||||
CGRect fromRect = referenceView.frame;// [referenceView convertRect:referenceView.bounds toView:nil];
|
||||
editorController.beginTransitionIn = ^UIView *(CGRect *referenceFrame, UIView **parentView)
|
||||
{
|
||||
*referenceFrame = fromRect;
|
||||
*parentView = referenceView.superview;
|
||||
//UIImageView *imageView = [[UIImageView alloc] initWithFrame:fromRect];
|
||||
//imageView.image = image;
|
||||
|
||||
return referenceView;
|
||||
};
|
||||
|
||||
editorController.beginTransitionOut = ^UIView *(CGRect *referenceFrame, UIView **parentView)
|
||||
{
|
||||
CGRect startFrame = CGRectZero;
|
||||
if (referenceFrame != NULL)
|
||||
{
|
||||
startFrame = *referenceFrame;
|
||||
*referenceFrame = fromRect;
|
||||
*parentView = referenceView.superview;
|
||||
}
|
||||
|
||||
if (beginTransitionOut) {
|
||||
beginTransitionOut();
|
||||
}
|
||||
|
||||
return referenceView;
|
||||
};
|
||||
|
||||
__weak TGPhotoEditorController *weakController = editorController;
|
||||
editorController.finishedTransitionOut = ^(bool saved) {
|
||||
TGPhotoEditorController *strongGalleryController = weakController;
|
||||
if (strongGalleryController != nil && strongGalleryController.overlayWindow == nil)
|
||||
{
|
||||
TGNavigationController *navigationController = (TGNavigationController *)strongGalleryController.navigationController;
|
||||
TGOverlayControllerWindow *window = (TGOverlayControllerWindow *)navigationController.view.window;
|
||||
if ([window isKindOfClass:[TGOverlayControllerWindow class]])
|
||||
[window dismiss];
|
||||
}
|
||||
if (finishTransitionOut) {
|
||||
finishTransitionOut();
|
||||
}
|
||||
};
|
||||
|
||||
editorController.didFinishRenderingFullSizeImage = ^(UIImage *resultImage)
|
||||
{
|
||||
fullSizeCompletion(resultImage);
|
||||
};
|
||||
|
||||
editorController.didFinishEditing = ^(id<TGMediaEditAdjustments> adjustments, UIImage *resultImage, __unused UIImage *thumbnailImage, __unused bool hasChanges, void(^commit)(void))
|
||||
{
|
||||
if (!hasChanges)
|
||||
return;
|
||||
|
||||
__strong TGPhotoEditorController *strongController = weakController;
|
||||
if (strongController == nil)
|
||||
return;
|
||||
|
||||
completion(resultImage, adjustments);
|
||||
|
||||
};
|
||||
editorController.requestThumbnailImage = ^(id<TGMediaEditableItem> editableItem)
|
||||
{
|
||||
return [editableItem thumbnailImageSignal];
|
||||
};
|
||||
|
||||
editorController.requestOriginalScreenSizeImage = ^(id<TGMediaEditableItem> editableItem, NSTimeInterval position)
|
||||
{
|
||||
return [editableItem screenImageSignal:position];
|
||||
};
|
||||
|
||||
editorController.requestOriginalFullSizeImage = ^(id<TGMediaEditableItem> editableItem, NSTimeInterval position)
|
||||
{
|
||||
return [editableItem originalImageSignal:position];
|
||||
};
|
||||
|
||||
TGOverlayControllerWindow *controllerWindow = [[TGOverlayControllerWindow alloc] initWithManager:windowManager parentController:controller contentController:editorController];
|
||||
controllerWindow.hidden = false;
|
||||
controller.view.clipsToBounds = true;
|
||||
|
||||
// TGModernGalleryController *galleryController = [[TGModernGalleryController alloc] initWithContext:windowContext];
|
||||
// galleryController.adjustsStatusBarVisibility = true;
|
||||
// galleryController.animateTransition = false;
|
||||
// galleryController.finishedTransitionIn = ^(id<TGModernGalleryItem> item, TGModernGalleryItemView *itemView) {
|
||||
// appeared();
|
||||
// };
|
||||
// //galleryController.hasFadeOutTransition = true;
|
||||
//
|
||||
// id<TGModernGalleryEditableItem> galleryItem = nil;
|
||||
// if (item.isVideo)
|
||||
// galleryItem = [[TGMediaPickerGalleryVideoItem alloc] initWithAsset:item];
|
||||
// else
|
||||
// galleryItem = [[TGMediaPickerGalleryPhotoItem alloc] initWithAsset:item];
|
||||
// galleryItem.editingContext = editingContext;
|
||||
// galleryItem.stickersContext = stickersContext;
|
||||
//
|
||||
// TGMediaPickerGalleryModel *model = [[TGMediaPickerGalleryModel alloc] initWithContext:windowContext items:@[galleryItem] focusItem:galleryItem selectionContext:nil editingContext:editingContext hasCaptions:true allowCaptionEntities:true hasTimer:false onlyCrop:false inhibitDocumentCaptions:false hasSelectionPanel:false hasCamera:false recipientName:recipientName];
|
||||
// model.controller = galleryController;
|
||||
// model.stickersContext = stickersContext;
|
||||
//
|
||||
// model.willFinishEditingItem = ^(id<TGMediaEditableItem> editableItem, id<TGMediaEditAdjustments> adjustments, id representation, bool hasChanges)
|
||||
// {
|
||||
// if (hasChanges)
|
||||
// {
|
||||
// [editingContext setAdjustments:adjustments forItem:editableItem];
|
||||
// [editingContext setTemporaryRep:representation forItem:editableItem];
|
||||
// }
|
||||
// };
|
||||
//
|
||||
// model.didFinishEditingItem = ^(id<TGMediaEditableItem> editableItem, __unused id<TGMediaEditAdjustments> adjustments, UIImage *resultImage, UIImage *thumbnailImage)
|
||||
// {
|
||||
// [editingContext setImage:resultImage thumbnailImage:thumbnailImage forItem:editableItem synchronous:false];
|
||||
// };
|
||||
//
|
||||
// model.saveItemCaption = ^(id<TGMediaEditableItem> editableItem, NSAttributedString *caption)
|
||||
// {
|
||||
// [editingContext setCaption:caption forItem:editableItem];
|
||||
// };
|
||||
//
|
||||
// model.didFinishRenderingFullSizeImage = ^(id<TGMediaEditableItem> editableItem, UIImage *resultImage)
|
||||
// {
|
||||
// [editingContext setFullSizeImage:resultImage forItem:editableItem];
|
||||
// };
|
||||
//
|
||||
// model.interfaceView.hasSwipeGesture = false;
|
||||
// galleryController.model = model;
|
||||
//
|
||||
// __weak TGModernGalleryController *weakGalleryController = galleryController;
|
||||
//
|
||||
// [model.interfaceView updateSelectionInterface:1 counterVisible:false animated:false];
|
||||
// model.interfaceView.thumbnailSignalForItem = ^SSignal *(id item)
|
||||
// {
|
||||
// return nil;
|
||||
// };
|
||||
// model.interfaceView.donePressed = ^(TGMediaPickerGalleryItem *item)
|
||||
// {
|
||||
// __strong TGModernGalleryController *strongController = weakGalleryController;
|
||||
// if (strongController == nil)
|
||||
// return;
|
||||
//
|
||||
// if ([item isKindOfClass:[TGMediaPickerGalleryVideoItem class]])
|
||||
// {
|
||||
// TGMediaPickerGalleryVideoItemView *itemView = (TGMediaPickerGalleryVideoItemView *)[strongController itemViewForItem:item];
|
||||
// [itemView stop];
|
||||
// [itemView setPlayButtonHidden:true animated:true];
|
||||
// }
|
||||
//
|
||||
// if (completion != nil)
|
||||
// completion(item.asset, editingContext);
|
||||
//
|
||||
// [strongController dismissWhenReadyAnimated:true];
|
||||
// };
|
||||
//
|
||||
// galleryController.beginTransitionIn = ^UIView *(__unused TGMediaPickerGalleryItem *item, __unused TGModernGalleryItemView *itemView)
|
||||
// {
|
||||
// return nil;
|
||||
// };
|
||||
//
|
||||
// galleryController.beginTransitionOut = ^UIView *(__unused TGMediaPickerGalleryItem *item, __unused TGModernGalleryItemView *itemView)
|
||||
// {
|
||||
// return nil;
|
||||
// };
|
||||
//
|
||||
// galleryController.completedTransitionOut = ^
|
||||
// {
|
||||
// TGModernGalleryController *strongGalleryController = weakGalleryController;
|
||||
// if (strongGalleryController != nil && strongGalleryController.overlayWindow == nil)
|
||||
// {
|
||||
// TGNavigationController *navigationController = (TGNavigationController *)strongGalleryController.navigationController;
|
||||
// TGOverlayControllerWindow *window = (TGOverlayControllerWindow *)navigationController.view.window;
|
||||
// if ([window isKindOfClass:[TGOverlayControllerWindow class]])
|
||||
// [window dismiss];
|
||||
// }
|
||||
// if (dismissed) {
|
||||
// dismissed();
|
||||
// }
|
||||
// };
|
||||
//
|
||||
// if (paint || adjustments) {
|
||||
// [model.interfaceView immediateEditorTransitionIn];
|
||||
// }
|
||||
//
|
||||
// for (UIView *view in snapshots) {
|
||||
// [galleryController.view addSubview:view];
|
||||
// }
|
||||
//
|
||||
// TGOverlayControllerWindow *controllerWindow = [[TGOverlayControllerWindow alloc] initWithManager:windowManager parentController:controller contentController:galleryController];
|
||||
// controllerWindow.hidden = false;
|
||||
// galleryController.view.clipsToBounds = true;
|
||||
//
|
||||
// if (paint) {
|
||||
// TGDispatchAfter(0.05, dispatch_get_main_queue(), ^{
|
||||
// [model presentPhotoEditorForItem:galleryItem tab:TGPhotoEditorPaintTab snapshots:snapshots fromRect:fromRect];
|
||||
// });
|
||||
// } else if (adjustments) {
|
||||
// TGDispatchAfter(0.05, dispatch_get_main_queue(), ^{
|
||||
// [model presentPhotoEditorForItem:galleryItem tab:TGPhotoEditorToolsTab snapshots:snapshots fromRect:fromRect];
|
||||
// });
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
|
@ -58,7 +58,47 @@ public enum LegacyAttachmentMenuMediaEditing {
|
||||
case file
|
||||
}
|
||||
|
||||
public func legacyMediaEditor(context: AccountContext, peer: Peer, threadTitle: String?, media: AnyMediaReference, initialCaption: NSAttributedString, snapshots: [UIView], transitionCompletion: (() -> Void)?, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, present: @escaping (ViewController, Any?) -> Void) {
|
||||
public enum LegacyMediaEditorMode {
|
||||
case draw
|
||||
case adjustments
|
||||
}
|
||||
|
||||
|
||||
public func legacyWallpaperEditor(context: AccountContext, item: TGMediaEditableItem, cropRect: CGRect, adjustments: TGMediaEditAdjustments?, referenceView: UIView, beginTransitionOut: (() -> Void)?, finishTransitionOut: (() -> Void)?, completion: @escaping (UIImage?, TGMediaEditAdjustments?) -> Void, fullSizeCompletion: @escaping (UIImage?) -> Void, present: @escaping (ViewController, Any?) -> Void) {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil)
|
||||
legacyController.blocksBackgroundWhenInOverlay = true
|
||||
legacyController.acceptsFocusWhenInOverlay = true
|
||||
legacyController.statusBar.statusBarStyle = .Ignore
|
||||
legacyController.controllerLoaded = { [weak legacyController] in
|
||||
legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true
|
||||
}
|
||||
|
||||
let emptyController = LegacyEmptyController(context: legacyController.context)!
|
||||
emptyController.navigationBarShouldBeHidden = true
|
||||
let navigationController = makeLegacyNavigationController(rootController: emptyController)
|
||||
navigationController.setNavigationBarHidden(true, animated: false)
|
||||
legacyController.bind(controller: navigationController)
|
||||
|
||||
legacyController.enableSizeClassSignal = true
|
||||
|
||||
present(legacyController, nil)
|
||||
|
||||
TGPhotoVideoEditor.present(with: legacyController.context, controller: emptyController, with: item, cropRect: cropRect, adjustments: adjustments, referenceView: referenceView, completion: { image, adjustments in
|
||||
completion(image, adjustments)
|
||||
}, fullSizeCompletion: { image in
|
||||
Queue.mainQueue().async {
|
||||
fullSizeCompletion(image)
|
||||
}
|
||||
}, beginTransitionOut: {
|
||||
beginTransitionOut?()
|
||||
}, finishTransitionOut: { [weak legacyController] in
|
||||
legacyController?.dismiss()
|
||||
finishTransitionOut?()
|
||||
})
|
||||
}
|
||||
|
||||
public func legacyMediaEditor(context: AccountContext, peer: Peer, threadTitle: String?, media: AnyMediaReference, mode: LegacyMediaEditorMode, initialCaption: NSAttributedString, snapshots: [UIView], transitionCompletion: (() -> Void)?, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, present: @escaping (ViewController, Any?) -> Void) {
|
||||
let _ = (fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: media)
|
||||
|> deliverOnMainQueue).start(next: { (value, isImage) in
|
||||
guard case let .data(data) = value, data.complete else {
|
||||
@ -107,7 +147,7 @@ public func legacyMediaEditor(context: AccountContext, peer: Peer, threadTitle:
|
||||
|
||||
present(legacyController, nil)
|
||||
|
||||
TGPhotoVideoEditor.present(with: legacyController.context, controller: emptyController, caption: initialCaption, withItem: item, paint: true, recipientName: recipientName, stickersContext: paintStickersContext, snapshots: snapshots as [Any], immediate: transitionCompletion != nil, appeared: {
|
||||
TGPhotoVideoEditor.present(with: legacyController.context, controller: emptyController, caption: initialCaption, withItem: item, paint: mode == .draw, adjustments: mode == .adjustments, recipientName: recipientName, stickersContext: paintStickersContext, from: .zero, mainSnapshot: nil, snapshots: snapshots as [Any], immediate: transitionCompletion != nil, appeared: {
|
||||
transitionCompletion?()
|
||||
}, completion: { result, editingContext in
|
||||
let nativeGenerator = legacyAssetPickerItemGenerator()
|
||||
@ -365,7 +405,7 @@ public func legacyAttachmentMenu(context: AccountContext, peer: Peer, threadTitl
|
||||
|
||||
present(legacyController, nil)
|
||||
|
||||
TGPhotoVideoEditor.present(with: legacyController.context, controller: emptyController, caption: NSAttributedString(), withItem: item, paint: false, recipientName: recipientName, stickersContext: paintStickersContext, snapshots: [], immediate: false, appeared: {
|
||||
TGPhotoVideoEditor.present(with: legacyController.context, controller: emptyController, caption: NSAttributedString(), withItem: item, paint: false, adjustments: false, recipientName: recipientName, stickersContext: paintStickersContext, from: .zero, mainSnapshot: nil, snapshots: [], immediate: false, appeared: {
|
||||
}, completion: { result, editingContext in
|
||||
let nativeGenerator = legacyAssetPickerItemGenerator()
|
||||
var selectableResult: TGMediaSelectableItem?
|
||||
|
@ -5,7 +5,7 @@ import Display
|
||||
import TelegramPresentationData
|
||||
|
||||
private let titleFont = Font.bold(13.0)
|
||||
private let actionFont = Font.medium(13.0)
|
||||
private let actionFont = Font.regular(13.0)
|
||||
|
||||
public enum ListSectionHeaderActionType {
|
||||
case generic
|
||||
@ -13,6 +13,7 @@ public enum ListSectionHeaderActionType {
|
||||
}
|
||||
|
||||
public final class ListSectionHeaderNode: ASDisplayNode {
|
||||
private let backgroundLayer: SimpleLayer
|
||||
private let label: ImmediateTextNode
|
||||
private var actionButtonLabel: ImmediateTextNode?
|
||||
private var actionButton: HighlightableButtonNode?
|
||||
@ -87,16 +88,29 @@ public final class ListSectionHeaderNode: ASDisplayNode {
|
||||
public init(theme: PresentationTheme) {
|
||||
self.theme = theme
|
||||
|
||||
self.backgroundLayer = SimpleLayer()
|
||||
|
||||
self.label = ImmediateTextNode()
|
||||
self.label.isUserInteractionEnabled = false
|
||||
self.label.isAccessibilityElement = true
|
||||
|
||||
super.init()
|
||||
|
||||
|
||||
self.layer.addSublayer(self.backgroundLayer)
|
||||
|
||||
self.addSubnode(self.label)
|
||||
|
||||
self.backgroundColor = theme.chatList.sectionHeaderFillColor
|
||||
self.backgroundLayer.backgroundColor = theme.chatList.sectionHeaderFillColor.cgColor
|
||||
}
|
||||
|
||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if let actionButton = self.actionButton {
|
||||
if actionButton.frame.contains(point) {
|
||||
return actionButton.view
|
||||
}
|
||||
}
|
||||
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
public func updateTheme(theme: PresentationTheme) {
|
||||
@ -105,7 +119,7 @@ public final class ListSectionHeaderNode: ASDisplayNode {
|
||||
|
||||
self.label.attributedText = NSAttributedString(string: self.title ?? "", font: titleFont, textColor: self.theme.chatList.sectionHeaderTextColor)
|
||||
|
||||
self.backgroundColor = theme.chatList.sectionHeaderFillColor
|
||||
self.backgroundLayer.backgroundColor = theme.chatList.sectionHeaderFillColor.cgColor
|
||||
if let action = self.action {
|
||||
self.actionButtonLabel?.attributedText = NSAttributedString(string: action, font: actionFont, textColor: self.theme.chatList.sectionHeaderTextColor)
|
||||
}
|
||||
@ -126,6 +140,8 @@ public final class ListSectionHeaderNode: ASDisplayNode {
|
||||
actionButtonLabel.frame = CGRect(origin: CGPoint(x: size.width - rightInset - 16.0 - buttonSize.width, y: 6.0 + UIScreenPixel), size: buttonSize)
|
||||
actionButton.frame = CGRect(origin: CGPoint(x: size.width - rightInset - 16.0 - buttonSize.width, y: 6.0 + UIScreenPixel), size: buttonSize)
|
||||
}
|
||||
|
||||
self.backgroundLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: size.width, height: size.height + UIScreenPixel))
|
||||
}
|
||||
|
||||
@objc private func actionButtonPressed() {
|
||||
|
@ -829,7 +829,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
||||
text = strongSelf.presentationData.strings.Location_ProximityTip(EnginePeer(peer).compactDisplayTitle).string
|
||||
}
|
||||
|
||||
strongSelf.interaction.present(TooltipScreen(account: strongSelf.context.account, text: text, icon: nil, location: .point(location.offsetBy(dx: -9.0, dy: 0.0), .right), displayDuration: .custom(3.0), shouldDismissOnTouch: { _ in
|
||||
strongSelf.interaction.present(TooltipScreen(account: strongSelf.context.account, sharedContext: strongSelf.context.sharedContext, text: text, icon: nil, location: .point(location.offsetBy(dx: -9.0, dy: 0.0), .right), displayDuration: .custom(3.0), shouldDismissOnTouch: { _ in
|
||||
return .dismiss(consume: false)
|
||||
}))
|
||||
})
|
||||
|
@ -75,7 +75,7 @@ public final class MeshWriteBuffer {
|
||||
}
|
||||
|
||||
public func seek(offset: Int) {
|
||||
self.file.seek(position: Int64(offset))
|
||||
let _ = self.file.seek(position: Int64(offset))
|
||||
self.offset = offset
|
||||
}
|
||||
}
|
||||
|
@ -99,12 +99,14 @@ public final class ManagedFile {
|
||||
return result
|
||||
}
|
||||
|
||||
public func seek(position: Int64) {
|
||||
@discardableResult
|
||||
public func seek(position: Int64) -> Bool {
|
||||
if let queue = self.queue {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
assert(!self.isClosed)
|
||||
lseek(self.fd, position, SEEK_SET)
|
||||
let result = lseek(self.fd, position, SEEK_SET)
|
||||
return result == position
|
||||
}
|
||||
|
||||
public func truncate(count: Int64) {
|
||||
|
@ -5,12 +5,16 @@ import Photos
|
||||
import AVFoundation
|
||||
|
||||
class MediaAssetsContext: NSObject, PHPhotoLibraryChangeObserver {
|
||||
private let assetType: PHAssetMediaType?
|
||||
|
||||
private var registeredChangeObserver = false
|
||||
private let changeSink = ValuePipe<PHChange>()
|
||||
private let mediaAccessSink = ValuePipe<PHAuthorizationStatus>()
|
||||
private let cameraAccessSink = ValuePipe<AVAuthorizationStatus?>()
|
||||
|
||||
override init() {
|
||||
init(assetType: PHAssetMediaType?) {
|
||||
self.assetType = assetType
|
||||
|
||||
super.init()
|
||||
|
||||
if PHPhotoLibrary.authorizationStatus() == .authorized {
|
||||
@ -30,7 +34,12 @@ class MediaAssetsContext: NSObject, PHPhotoLibraryChangeObserver {
|
||||
}
|
||||
|
||||
func fetchAssets(_ collection: PHAssetCollection) -> Signal<PHFetchResult<PHAsset>, NoError> {
|
||||
let initialFetchResult = PHAsset.fetchAssets(in: collection, options: nil)
|
||||
let options = PHFetchOptions()
|
||||
if let assetType = self.assetType {
|
||||
options.predicate = NSPredicate(format: "mediaType = %d", assetType.rawValue)
|
||||
}
|
||||
|
||||
let initialFetchResult = PHAsset.fetchAssets(in: collection, options: options)
|
||||
let fetchResult = Atomic<PHFetchResult<PHAsset>>(value: initialFetchResult)
|
||||
return .single(initialFetchResult)
|
||||
|> then(
|
||||
|
@ -323,7 +323,14 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
strongSelf.updateHasSpoiler(hasSpoiler)
|
||||
}))
|
||||
|
||||
if asset.mediaType == .video {
|
||||
if asset.isFavorite {
|
||||
self.typeIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Favorite"), color: .white)
|
||||
if self.typeIconNode.supernode == nil {
|
||||
self.addSubnode(self.gradientNode)
|
||||
self.addSubnode(self.typeIconNode)
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
} else if asset.mediaType == .video {
|
||||
if asset.mediaSubtypes.contains(.videoHighFrameRate) {
|
||||
self.typeIconNode.image = UIImage(bundleImageName: "Media Editor/MediaSlomo")
|
||||
} else if asset.mediaSubtypes.contains(.videoTimelapse) {
|
||||
|
@ -128,7 +128,12 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
}
|
||||
}
|
||||
|
||||
case assets(PHAssetCollection?, Bool)
|
||||
public enum AssetsMode: Equatable {
|
||||
case `default`
|
||||
case wallpaper
|
||||
}
|
||||
|
||||
case assets(PHAssetCollection?, AssetsMode)
|
||||
case media([Media])
|
||||
}
|
||||
|
||||
@ -238,7 +243,11 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
self.controller = controller
|
||||
self.presentationData = controller.presentationData
|
||||
|
||||
let mediaAssetsContext = MediaAssetsContext()
|
||||
var assetType: PHAssetMediaType?
|
||||
if case let .assets(_, mode) = controller.subject, case .wallpaper = mode {
|
||||
assetType = .image
|
||||
}
|
||||
let mediaAssetsContext = MediaAssetsContext(assetType: assetType)
|
||||
self.mediaAssetsContext = mediaAssetsContext
|
||||
|
||||
self.containerNode = ASDisplayNode()
|
||||
@ -253,7 +262,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
|
||||
super.init()
|
||||
|
||||
if case .assets(nil, false) = controller.subject {
|
||||
if case .assets(nil, .default) = controller.subject {
|
||||
} else {
|
||||
self.preloadPromise.set(false)
|
||||
}
|
||||
@ -376,32 +385,40 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
|
||||
self.gridNode.scrollView.alwaysBounceVertical = true
|
||||
self.gridNode.scrollView.showsVerticalScrollIndicator = false
|
||||
|
||||
let selectionGesture = MediaPickerGridSelectionGesture<TGMediaSelectableItem>()
|
||||
selectionGesture.delegate = self
|
||||
selectionGesture.began = { [weak self] in
|
||||
self?.controller?.cancelPanGesture()
|
||||
}
|
||||
selectionGesture.updateIsScrollEnabled = { [weak self] isEnabled in
|
||||
self?.gridNode.scrollView.isScrollEnabled = isEnabled
|
||||
}
|
||||
selectionGesture.itemAt = { [weak self] point in
|
||||
if let strongSelf = self, let itemNode = strongSelf.gridNode.itemNodeAtPoint(point) as? MediaPickerGridItemNode, let selectableItem = itemNode.selectableItem {
|
||||
return (selectableItem, strongSelf.controller?.interaction?.selectionState?.isIdentifierSelected(selectableItem.uniqueIdentifier) ?? false)
|
||||
} else {
|
||||
return nil
|
||||
if case let .assets(_, mode) = controller.subject, case .wallpaper = mode {
|
||||
|
||||
} else {
|
||||
let selectionGesture = MediaPickerGridSelectionGesture<TGMediaSelectableItem>()
|
||||
selectionGesture.delegate = self
|
||||
selectionGesture.began = { [weak self] in
|
||||
self?.controller?.cancelPanGesture()
|
||||
}
|
||||
}
|
||||
selectionGesture.updateSelection = { [weak self] asset, selected in
|
||||
if let strongSelf = self {
|
||||
strongSelf.controller?.interaction?.selectionState?.setItem(asset, selected: selected, animated: true, sender: nil)
|
||||
selectionGesture.updateIsScrollEnabled = { [weak self] isEnabled in
|
||||
self?.gridNode.scrollView.isScrollEnabled = isEnabled
|
||||
}
|
||||
selectionGesture.itemAt = { [weak self] point in
|
||||
if let strongSelf = self, let itemNode = strongSelf.gridNode.itemNodeAtPoint(point) as? MediaPickerGridItemNode, let selectableItem = itemNode.selectableItem {
|
||||
return (selectableItem, strongSelf.controller?.interaction?.selectionState?.isIdentifierSelected(selectableItem.uniqueIdentifier) ?? false)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
selectionGesture.updateSelection = { [weak self] asset, selected in
|
||||
if let strongSelf = self {
|
||||
strongSelf.controller?.interaction?.selectionState?.setItem(asset, selected: selected, animated: true, sender: nil)
|
||||
}
|
||||
}
|
||||
selectionGesture.sideInset = 44.0
|
||||
self.gridNode.view.addGestureRecognizer(selectionGesture)
|
||||
self.selectionGesture = selectionGesture
|
||||
}
|
||||
selectionGesture.sideInset = 44.0
|
||||
self.gridNode.view.addGestureRecognizer(selectionGesture)
|
||||
self.selectionGesture = selectionGesture
|
||||
|
||||
if let controller = self.controller, case let .assets(collection, _) = controller.subject, collection != nil {
|
||||
self.gridNode.view.interactiveTransitionGestureRecognizerTest = { point -> Bool in
|
||||
@ -454,7 +471,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
}
|
||||
})
|
||||
|
||||
if let controller = self.controller, case .assets(nil, false) = controller.subject {
|
||||
if let controller = self.controller, case .assets(nil, .default) = controller.subject {
|
||||
let enableAnimations = self.controller?.context.sharedContext.energyUsageSettings.fullTranslucency ?? true
|
||||
|
||||
let cameraView = TGAttachmentCameraView(forSelfPortrait: false, videoModeByDefault: controller.bannedSendPhotos != nil && controller.bannedSendVideos == nil)!
|
||||
@ -556,7 +573,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
var updateLayout = false
|
||||
|
||||
var selectable = true
|
||||
if case let .assets(_, isStandalone) = controller.subject, isStandalone {
|
||||
if case let .assets(_, mode) = controller.subject, mode != .default {
|
||||
selectable = false
|
||||
}
|
||||
|
||||
@ -732,7 +749,11 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
}
|
||||
|
||||
if let customSelection = controller.customSelection {
|
||||
self.openingMedia = true
|
||||
customSelection(fetchResult[index])
|
||||
Queue.mainQueue().after(0.3) {
|
||||
self.openingMedia = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -1229,7 +1250,24 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
|
||||
private var isDismissing = false
|
||||
|
||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peer: EnginePeer?, threadTitle: String?, chatLocation: ChatLocation?, bannedSendPhotos: (Int32, Bool)?, bannedSendVideos: (Int32, Bool)?, subject: Subject, editingContext: TGMediaEditingContext? = nil, selectionContext: TGMediaSelectionContext? = nil, saveEditedPhotos: Bool = false) {
|
||||
fileprivate let mainButtonState: AttachmentMainButtonState?
|
||||
private let mainButtonAction: (() -> Void)?
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
|
||||
peer: EnginePeer?,
|
||||
threadTitle: String?,
|
||||
chatLocation: ChatLocation?,
|
||||
bannedSendPhotos: (Int32, Bool)?,
|
||||
bannedSendVideos: (Int32, Bool)?,
|
||||
subject: Subject,
|
||||
editingContext: TGMediaEditingContext? = nil,
|
||||
selectionContext: TGMediaSelectionContext? = nil,
|
||||
saveEditedPhotos: Bool = false,
|
||||
mainButtonState: AttachmentMainButtonState? = nil,
|
||||
mainButtonAction: (() -> Void)? = nil
|
||||
) {
|
||||
self.context = context
|
||||
|
||||
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
|
||||
@ -1242,13 +1280,24 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
self.bannedSendVideos = bannedSendVideos
|
||||
self.subject = subject
|
||||
self.saveEditedPhotos = saveEditedPhotos
|
||||
self.mainButtonState = mainButtonState
|
||||
self.mainButtonAction = mainButtonAction
|
||||
|
||||
let selectionContext = selectionContext ?? TGMediaSelectionContext()
|
||||
|
||||
self.titleView = MediaPickerTitleView(theme: self.presentationData.theme, segments: [self.presentationData.strings.Attachment_AllMedia, self.presentationData.strings.Attachment_SelectedMedia(1)], selectedIndex: 0)
|
||||
|
||||
if case let .assets(collection, _) = subject, let collection = collection {
|
||||
self.titleView.title = collection.localizedTitle ?? presentationData.strings.Attachment_Gallery
|
||||
if case let .assets(collection, mode) = subject {
|
||||
if let collection = collection {
|
||||
self.titleView.title = collection.localizedTitle ?? presentationData.strings.Attachment_Gallery
|
||||
} else {
|
||||
switch mode {
|
||||
case .default:
|
||||
self.titleView.title = presentationData.strings.Attachment_Gallery
|
||||
case .wallpaper:
|
||||
self.titleView.title = presentationData.strings.Conversation_Theme_ChooseWallpaperTitle
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.titleView.title = presentationData.strings.Attachment_Gallery
|
||||
}
|
||||
@ -1316,7 +1365,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
|
||||
self.navigationItem.titleView = self.titleView
|
||||
|
||||
if case let .assets(_, isStandalone) = self.subject, isStandalone {
|
||||
if case let .assets(_, mode) = self.subject, mode != .default {
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
|
||||
} else {
|
||||
if case let .assets(collection, _) = self.subject, collection != nil {
|
||||
@ -1609,6 +1658,10 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
}
|
||||
}
|
||||
|
||||
func mainButtonPressed() {
|
||||
self.mainButtonAction?()
|
||||
}
|
||||
|
||||
func dismissAllTooltips() {
|
||||
self.undoOverlayController?.dismissWithCommitAction()
|
||||
}
|
||||
@ -1674,13 +1727,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
}
|
||||
|
||||
private func presentSearch(activateOnDisplay: Bool) {
|
||||
guard self.moreButtonNode.iconNode.iconState == .search else {
|
||||
guard self.moreButtonNode.iconNode.iconState == .search, case let .assets(_, mode) = self.subject else {
|
||||
return
|
||||
}
|
||||
self.requestAttachmentMenuExpansion()
|
||||
self.presentWebSearch(MediaGroupsScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, mediaAssetsContext: self.controllerNode.mediaAssetsContext, openGroup: { [weak self] collection in
|
||||
if let strongSelf = self {
|
||||
let mediaPicker = MediaPickerScreen(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: strongSelf.peer, threadTitle: strongSelf.threadTitle, chatLocation: strongSelf.chatLocation, bannedSendPhotos: strongSelf.bannedSendPhotos, bannedSendVideos: strongSelf.bannedSendVideos, subject: .assets(collection, false), editingContext: strongSelf.interaction?.editingState, selectionContext: strongSelf.interaction?.selectionState)
|
||||
let mediaPicker = MediaPickerScreen(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: strongSelf.peer, threadTitle: strongSelf.threadTitle, chatLocation: strongSelf.chatLocation, bannedSendPhotos: strongSelf.bannedSendPhotos, bannedSendVideos: strongSelf.bannedSendVideos, subject: .assets(collection, mode), editingContext: strongSelf.interaction?.editingState, selectionContext: strongSelf.interaction?.selectionState)
|
||||
|
||||
mediaPicker.presentSchedulePicker = strongSelf.presentSchedulePicker
|
||||
mediaPicker.presentTimerPicker = strongSelf.presentTimerPicker
|
||||
@ -1793,21 +1846,17 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
}
|
||||
|
||||
public var mediaPickerContext: AttachmentMediaPickerContext? {
|
||||
if let interaction = self.interaction {
|
||||
return MediaPickerContext(interaction: interaction)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
return MediaPickerContext(controller: self)
|
||||
}
|
||||
}
|
||||
|
||||
final class MediaPickerContext: AttachmentMediaPickerContext {
|
||||
private weak var interaction: MediaPickerInteraction?
|
||||
private weak var controller: MediaPickerScreen?
|
||||
|
||||
var selectionCount: Signal<Int, NoError> {
|
||||
return Signal { [weak self] subscriber in
|
||||
let disposable = self?.interaction?.selectionState?.selectionChangedSignal().start(next: { [weak self] value in
|
||||
subscriber.putNext(Int(self?.interaction?.selectionState?.count() ?? 0))
|
||||
let disposable = self?.controller?.interaction?.selectionState?.selectionChangedSignal().start(next: { [weak self] value in
|
||||
subscriber.putNext(Int(self?.controller?.interaction?.selectionState?.count() ?? 0))
|
||||
}, error: { _ in }, completed: { })
|
||||
return ActionDisposable {
|
||||
disposable?.dispose()
|
||||
@ -1817,7 +1866,7 @@ final class MediaPickerContext: AttachmentMediaPickerContext {
|
||||
|
||||
var caption: Signal<NSAttributedString?, NoError> {
|
||||
return Signal { [weak self] subscriber in
|
||||
let disposable = self?.interaction?.editingState.forcedCaption().start(next: { caption in
|
||||
let disposable = self?.controller?.interaction?.editingState.forcedCaption().start(next: { caption in
|
||||
if let caption = caption as? NSAttributedString {
|
||||
subscriber.putNext(caption)
|
||||
} else {
|
||||
@ -1835,27 +1884,27 @@ final class MediaPickerContext: AttachmentMediaPickerContext {
|
||||
}
|
||||
|
||||
public var mainButtonState: Signal<AttachmentMainButtonState?, NoError> {
|
||||
return .single(nil)
|
||||
return .single(self.controller?.mainButtonState)
|
||||
}
|
||||
|
||||
init(interaction: MediaPickerInteraction) {
|
||||
self.interaction = interaction
|
||||
init(controller: MediaPickerScreen) {
|
||||
self.controller = controller
|
||||
}
|
||||
|
||||
func setCaption(_ caption: NSAttributedString) {
|
||||
self.interaction?.editingState.setForcedCaption(caption, skipUpdate: true)
|
||||
self.controller?.interaction?.editingState.setForcedCaption(caption, skipUpdate: true)
|
||||
}
|
||||
|
||||
func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode) {
|
||||
self.interaction?.sendSelected(nil, mode == .silently, mode == .whenOnline ? scheduleWhenOnlineTimestamp : nil, true, {})
|
||||
self.controller?.interaction?.sendSelected(nil, mode == .silently, mode == .whenOnline ? scheduleWhenOnlineTimestamp : nil, true, {})
|
||||
}
|
||||
|
||||
func schedule() {
|
||||
self.interaction?.schedule()
|
||||
self.controller?.interaction?.schedule()
|
||||
}
|
||||
|
||||
func mainButtonAction() {
|
||||
|
||||
self.controller?.mainButtonPressed()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1968,3 +2017,28 @@ public class MediaPickerGridSelectionGesture<T> : UIPanGestureRecognizer {
|
||||
self.updateIsScrollEnabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
public func wallpaperMediaPickerController(
|
||||
context: AccountContext,
|
||||
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil,
|
||||
peer: EnginePeer,
|
||||
animateAppearance: Bool,
|
||||
completion: @escaping (PHAsset) -> Void = { _ in },
|
||||
openColors: @escaping () -> Void
|
||||
) -> ViewController {
|
||||
let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false, makeEntityInputView: {
|
||||
return nil
|
||||
})
|
||||
controller.animateAppearance = animateAppearance
|
||||
//controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
||||
controller.requestController = { [weak controller] _, present in
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let mediaPickerController = MediaPickerScreen(context: context, peer: nil, threadTitle: nil, chatLocation: nil, bannedSendPhotos: nil, bannedSendVideos: nil, subject: .assets(nil, .wallpaper), mainButtonState: AttachmentMainButtonState(text: presentationData.strings.Conversation_Theme_SetColorWallpaper, font: .regular, background: .color(.clear), textColor: presentationData.theme.actionSheet.controlAccentColor, isVisible: true, progress: .none, isEnabled: true), mainButtonAction: {
|
||||
controller?.dismiss(animated: true)
|
||||
openColors()
|
||||
})
|
||||
mediaPickerController.customSelection = completion
|
||||
present(mediaPickerController, mediaPickerController.mediaPickerContext)
|
||||
}
|
||||
return controller
|
||||
}
|
||||
|
@ -88,7 +88,7 @@ final class MediaPickerTitleView: UIView {
|
||||
let controlSize = self.segmentedControlNode.updateLayout(.stretchToFill(width: min(300.0, size.width - 36.0)), transition: .immediate)
|
||||
self.segmentedControlNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - controlSize.width) / 2.0), y: floorToScreenPixels((size.height - controlSize.height) / 2.0)), size: controlSize)
|
||||
|
||||
let titleSize = self.titleNode.updateLayout(CGSize(width: 160.0, height: 44.0))
|
||||
let titleSize = self.titleNode.updateLayout(CGSize(width: 210.0, height: 44.0))
|
||||
self.titleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize)
|
||||
}
|
||||
}
|
||||
|
@ -302,7 +302,7 @@ public func chatMessageBubbleMosaicLayout(maxSize: CGSize, itemSizes: [CGSize],
|
||||
innerPositionFlags.insert(.right)
|
||||
}
|
||||
|
||||
if positionFlags == .none {
|
||||
if positionFlags == [] {
|
||||
innerPositionFlags = .inside
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,7 @@
|
||||
@protocol MTTcpConnectionInterfaceDelegate <NSObject>
|
||||
|
||||
- (void)connectionInterfaceDidReadPartialDataOfLength:(NSUInteger)partialLength tag:(long)tag;
|
||||
- (void)connectionInterfaceDidReadData:(NSData * _Nonnull)rawData withTag:(long)tag;
|
||||
- (void)connectionInterfaceDidReadData:(NSData * _Nonnull)rawData withTag:(long)tag networkType:(int32_t)networkType;
|
||||
- (void)connectionInterfaceDidConnect;
|
||||
- (void)connectionInterfaceDidDisconnectWithError:(NSError * _Nullable)error;
|
||||
|
||||
|
@ -21,7 +21,7 @@
|
||||
- (MTMessageTransaction *)mtProtoMessageTransaction:(MTProto *)mtProto authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector sessionInfo:(MTSessionInfo *)sessionInfo scheme:(MTTransportScheme *)scheme;
|
||||
- (void)mtProtoDidChangeSession:(MTProto *)mtProto;
|
||||
- (void)mtProtoServerDidChangeSession:(MTProto *)mtProto firstValidMessageId:(int64_t)firstValidMessageId otherValidMessageIds:(NSArray *)otherValidMessageIds;
|
||||
- (void)mtProto:(MTProto *)mtProto receivedMessage:(MTIncomingMessage *)message authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector;
|
||||
- (void)mtProto:(MTProto *)mtProto receivedMessage:(MTIncomingMessage *)message authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector networkType:(int32_t)networkType;
|
||||
- (void)mtProto:(MTProto *)mtProto receivedQuickAck:(int32_t)quickAckId;
|
||||
- (void)mtProto:(MTProto *)mtProto transactionsMayHaveFailed:(NSArray *)transactionIds;
|
||||
- (void)mtProtoAllTransactionsMayHaveFailed:(MTProto *)mtProto;
|
||||
|
@ -6,6 +6,16 @@
|
||||
@class MTRequestErrorContext;
|
||||
@class MTRpcError;
|
||||
|
||||
@interface MTRequestResponseInfo : NSObject
|
||||
|
||||
@property (nonatomic, readonly) int32_t networkType;
|
||||
@property (nonatomic, readonly) double timestamp;
|
||||
@property (nonatomic, readonly) double duration;
|
||||
|
||||
- (instancetype)initWithNetworkType:(int32_t)networkType timestamp:(double)timestamp duration:(double)duration;
|
||||
|
||||
@end
|
||||
|
||||
@interface MTRequest : NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) id internalId;
|
||||
@ -24,7 +34,7 @@
|
||||
@property (nonatomic) bool passthroughPasswordEntryError;
|
||||
@property (nonatomic) bool needsTimeoutTimer;
|
||||
|
||||
@property (nonatomic, copy) void (^completed)(id result, NSTimeInterval completionTimestamp, MTRpcError *error);
|
||||
@property (nonatomic, copy) void (^completed)(id result, MTRequestResponseInfo *info, MTRpcError *error);
|
||||
@property (nonatomic, copy) void (^progressUpdated)(float progress, NSUInteger packetLength);
|
||||
@property (nonatomic, copy) void (^acknowledgementReceived)();
|
||||
|
||||
|
@ -12,6 +12,7 @@
|
||||
@property (nonatomic) bool delivered;
|
||||
@property (nonatomic) int64_t responseMessageId;
|
||||
@property (nonatomic) bool willInitializeApi;
|
||||
@property (nonatomic) double sentTimestamp;
|
||||
|
||||
- (instancetype)initWithMessageId:(int64_t)messageId messageSeqNo:(int32_t)messageSeqNo transactionId:(id)transactionId quickAckId:(int32_t)quickAckId;
|
||||
|
||||
|
@ -26,7 +26,7 @@
|
||||
- (void)transportConnectionProblemsStatusChanged:(MTTransport * _Nonnull)transport scheme:(MTTransportScheme * _Nonnull)scheme hasConnectionProblems:(bool)hasConnectionProblems isProbablyHttp:(bool)isProbablyHttp;
|
||||
|
||||
- (void)transportReadyForTransaction:(MTTransport * _Nonnull)transport scheme:(MTTransportScheme * _Nonnull)scheme transportSpecificTransaction:(MTMessageTransaction * _Nonnull)transportSpecificTransaction forceConfirmations:(bool)forceConfirmations transactionReady:(void (^ _Nonnull)(NSArray * _Nonnull))transactionReady;
|
||||
- (void)transportHasIncomingData:(MTTransport * _Nonnull)transport scheme:(MTTransportScheme * _Nonnull)scheme data:(NSData * _Nonnull)data transactionId:(id _Nonnull)transactionId requestTransactionAfterProcessing:(bool)requestTransactionAfterProcessing decodeResult:(void (^ _Nonnull)(id _Nonnull transactionId, bool success))decodeResult;
|
||||
- (void)transportHasIncomingData:(MTTransport * _Nonnull)transport scheme:(MTTransportScheme * _Nonnull)scheme networkType:(int32_t)networkType data:(NSData * _Nonnull)data transactionId:(id _Nonnull)transactionId requestTransactionAfterProcessing:(bool)requestTransactionAfterProcessing decodeResult:(void (^ _Nonnull)(id _Nonnull transactionId, bool success))decodeResult;
|
||||
- (void)transportTransactionsMayHaveFailed:(MTTransport * _Nonnull)transport transactionIds:(NSArray * _Nonnull)transactionIds;
|
||||
- (void)transportReceivedQuickAck:(MTTransport * _Nonnull)transport quickAckId:(int32_t)quickAckId;
|
||||
- (void)transportDecodeProgressToken:(MTTransport * _Nonnull)transport scheme:(MTTransportScheme * _Nonnull)scheme data:(NSData * _Nonnull)data token:(int64_t)token completion:(void (^ _Nonnull)(int64_t token, id _Nonnull progressToken))completion;
|
||||
@ -57,7 +57,7 @@
|
||||
- (void)stop;
|
||||
- (void)updateConnectionState;
|
||||
- (void)setDelegateNeedsTransaction;
|
||||
- (void)_processIncomingData:(NSData * _Nonnull)data scheme:(MTTransportScheme * _Nonnull)scheme transactionId:(id _Nonnull)transactionId requestTransactionAfterProcessing:(bool)requestTransactionAfterProcessing decodeResult:(void (^ _Nonnull)(id _Nonnull transactionId, bool success))decodeResult;
|
||||
- (void)_processIncomingData:(NSData * _Nonnull)data scheme:(MTTransportScheme * _Nonnull)scheme networkType:(int32_t)networkType transactionId:(id _Nonnull)transactionId requestTransactionAfterProcessing:(bool)requestTransactionAfterProcessing decodeResult:(void (^ _Nonnull)(id _Nonnull transactionId, bool success))decodeResult;
|
||||
- (void)_networkAvailabilityChanged:(bool)networkAvailable;
|
||||
|
||||
- (void)activeTransactionIds:(void (^ _Nonnull)(NSArray * _Nonnull activeTransactionId))completion;
|
||||
|
@ -975,7 +975,7 @@ typedef enum GCDAsyncSocketError GCDAsyncSocketError;
|
||||
* Called when a socket has completed reading the requested data into memory.
|
||||
* Not called if there is an error.
|
||||
**/
|
||||
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag;
|
||||
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag networkType:(int32_t)networkType;
|
||||
|
||||
/**
|
||||
* Called when a socket has read in data, but has not yet completed the read.
|
||||
|
@ -5119,14 +5119,15 @@ enum GCDAsyncSocketConfig
|
||||
result = [NSData dataWithBytesNoCopy:buffer length:currentRead->bytesDone freeWhenDone:NO];
|
||||
}
|
||||
|
||||
if (delegateQueue && [delegate respondsToSelector:@selector(socket:didReadData:withTag:)])
|
||||
if (delegateQueue && [delegate respondsToSelector:@selector(socket:didReadData:withTag:networkType:)])
|
||||
{
|
||||
__strong id theDelegate = delegate;
|
||||
GCDAsyncReadPacket *theRead = currentRead; // Ensure currentRead retained since result may not own buffer
|
||||
int32_t networkType = _interface == MTNetworkUsageManagerInterfaceOther ? 0 : 1;
|
||||
|
||||
dispatch_async(delegateQueue, ^{ @autoreleasepool {
|
||||
|
||||
[theDelegate socket:self didReadData:result withTag:theRead->tag];
|
||||
[theDelegate socket:self didReadData:result withTag:theRead->tag networkType:networkType];
|
||||
}});
|
||||
}
|
||||
|
||||
|
@ -313,7 +313,7 @@ static NSString *makeRandomPadding() {
|
||||
|
||||
__weak MTContext *weakCurrentContext = currentContext;
|
||||
return [[MTSignal alloc] initWithGenerator:^id<MTDisposable>(MTSubscriber *subscriber) {
|
||||
[request setCompleted:^(MTDatacenterAddressListData *result, __unused NSTimeInterval completionTimestamp, id error)
|
||||
[request setCompleted:^(MTDatacenterAddressListData *result, __unused MTRequestResponseInfo *info, id error)
|
||||
{
|
||||
if (error == nil) {
|
||||
__strong MTContext *strongCurrentContext = weakCurrentContext;
|
||||
|
@ -136,7 +136,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)mtProto:(MTProto *)mtProto receivedMessage:(MTIncomingMessage *)message authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector {
|
||||
- (void)mtProto:(MTProto *)mtProto receivedMessage:(MTIncomingMessage *)message authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector networkType:(int32_t)networkType {
|
||||
if ([message.body isKindOfClass:[MTRpcResultMessage class]]) {
|
||||
MTRpcResultMessage *rpcResultMessage = message.body;
|
||||
if (rpcResultMessage.requestMessageId == _currentMessageId) {
|
||||
|
@ -410,7 +410,7 @@ static NSData *encryptRSAModernPadding(id<EncryptionProvider> encryptionProvider
|
||||
}
|
||||
}
|
||||
|
||||
- (void)mtProto:(MTProto *)mtProto receivedMessage:(MTIncomingMessage *)message authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector
|
||||
- (void)mtProto:(MTProto *)mtProto receivedMessage:(MTIncomingMessage *)message authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector networkType:(int32_t)networkType
|
||||
{
|
||||
if (_stage == MTDatacenterAuthStagePQ && [message.body isKindOfClass:[MTResPqMessage class]])
|
||||
{
|
||||
|
@ -101,7 +101,7 @@
|
||||
[request setPayload:exportAuthRequestData metadata:@"exportAuthorization" shortMetadata:@"exportAuthorization" responseParser:responseParser];
|
||||
|
||||
__weak MTDatacenterTransferAuthAction *weakSelf = self;
|
||||
[request setCompleted:^(MTExportedAuthorizationData *result, __unused NSTimeInterval timestamp, id error)
|
||||
[request setCompleted:^(MTExportedAuthorizationData *result, __unused MTRequestResponseInfo *info, id error)
|
||||
{
|
||||
__strong MTDatacenterTransferAuthAction *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
@ -147,7 +147,7 @@
|
||||
id authToken = _authToken;
|
||||
|
||||
__weak MTDatacenterTransferAuthAction *weakSelf = self;
|
||||
[request setCompleted:^(__unused id result, __unused NSTimeInterval timestamp, id error)
|
||||
[request setCompleted:^(__unused id result, __unused MTRequestResponseInfo *info, id error)
|
||||
{
|
||||
__strong MTDatacenterTransferAuthAction *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
|
@ -106,7 +106,7 @@
|
||||
[request setPayload:getConfigData metadata:@"getConfig" shortMetadata:@"getConfig" responseParser:responseParser];
|
||||
|
||||
__weak MTDiscoverDatacenterAddressAction *weakSelf = self;
|
||||
[request setCompleted:^(MTDatacenterAddressListData *result, __unused NSTimeInterval completionTimestamp, id error)
|
||||
[request setCompleted:^(MTDatacenterAddressListData *result, __unused MTRequestResponseInfo *info, id error)
|
||||
{
|
||||
__strong MTDiscoverDatacenterAddressAction *strongSelf = weakSelf;
|
||||
if (strongSelf != nil) {
|
||||
|
@ -1966,7 +1966,7 @@ static NSString *dumpHexString(NSData *data, int maxLength) {
|
||||
return hexString;
|
||||
}
|
||||
|
||||
- (void)transportHasIncomingData:(MTTransport *)transport scheme:(MTTransportScheme *)scheme data:(NSData *)data transactionId:(id)transactionId requestTransactionAfterProcessing:(bool)requestTransactionAfterProcessing decodeResult:(void (^)(id transactionId, bool success))decodeResult
|
||||
- (void)transportHasIncomingData:(MTTransport *)transport scheme:(MTTransportScheme *)scheme networkType:(int32_t)networkType data:(NSData *)data transactionId:(id)transactionId requestTransactionAfterProcessing:(bool)requestTransactionAfterProcessing decodeResult:(void (^)(id transactionId, bool success))decodeResult
|
||||
{
|
||||
/*__block bool simulateError = false;
|
||||
static dispatch_once_t onceToken;
|
||||
@ -2097,7 +2097,7 @@ static NSString *dumpHexString(NSData *data, int maxLength) {
|
||||
|
||||
for (MTIncomingMessage *incomingMessage in parsedMessages)
|
||||
{
|
||||
[self _processIncomingMessage:incomingMessage totalSize:(int)data.length withTransactionId:transactionId address:scheme.address authInfoSelector:authInfoSelector];
|
||||
[self _processIncomingMessage:incomingMessage totalSize:(int)data.length withTransactionId:transactionId address:scheme.address authInfoSelector:authInfoSelector networkType:networkType];
|
||||
}
|
||||
|
||||
if (requestTransactionAfterProcessing)
|
||||
@ -2422,7 +2422,7 @@ static bool isDataEqualToDataConstTime(NSData *data1, NSData *data2) {
|
||||
return messages;
|
||||
}
|
||||
|
||||
- (void)_processIncomingMessage:(MTIncomingMessage *)incomingMessage totalSize:(int)totalSize withTransactionId:(id)transactionId address:(MTDatacenterAddress *)address authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector
|
||||
- (void)_processIncomingMessage:(MTIncomingMessage *)incomingMessage totalSize:(int)totalSize withTransactionId:(id)transactionId address:(MTDatacenterAddress *)address authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector networkType:(int32_t)networkType
|
||||
{
|
||||
if ([_sessionInfo messageProcessed:incomingMessage.messageId])
|
||||
{
|
||||
@ -2605,8 +2605,8 @@ static bool isDataEqualToDataConstTime(NSData *data1, NSData *data2) {
|
||||
{
|
||||
id<MTMessageService> messageService = _messageServices[(NSUInteger)i];
|
||||
|
||||
if ([messageService respondsToSelector:@selector(mtProto:receivedMessage:authInfoSelector:)])
|
||||
[messageService mtProto:self receivedMessage:incomingMessage authInfoSelector:authInfoSelector];
|
||||
if ([messageService respondsToSelector:@selector(mtProto:receivedMessage:authInfoSelector:networkType:)])
|
||||
[messageService mtProto:self receivedMessage:incomingMessage authInfoSelector:authInfoSelector networkType:networkType];
|
||||
}
|
||||
|
||||
if (_timeFixContext != nil && [incomingMessage.body isKindOfClass:[MTPongMessage class]] && ((MTPongMessage *)incomingMessage.body).messageId == _timeFixContext.messageId)
|
||||
|
@ -5,6 +5,20 @@
|
||||
#import <os/lock.h>
|
||||
#import <libkern/OSAtomic.h>
|
||||
|
||||
@implementation MTRequestResponseInfo
|
||||
|
||||
- (instancetype)initWithNetworkType:(int32_t)networkType timestamp:(double)timestamp duration:(double)duration {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_networkType = networkType;
|
||||
_timestamp = timestamp;
|
||||
_duration = duration;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface MTRequestInternalId : NSObject <NSCopying>
|
||||
{
|
||||
NSUInteger _value;
|
||||
|
@ -369,7 +369,7 @@
|
||||
MTRequestNoopParser responseParser = [[_context serialization] requestNoop:&noopData];
|
||||
[request setPayload:noopData metadata:@"noop" shortMetadata:@"noop" responseParser:responseParser];
|
||||
|
||||
[request setCompleted:^(__unused id result, __unused NSTimeInterval timestamp, __unused id error) {
|
||||
[request setCompleted:^(__unused id result, __unused MTRequestResponseInfo *info, __unused id error) {
|
||||
}];
|
||||
|
||||
[self addRequest:request];
|
||||
@ -614,6 +614,7 @@
|
||||
}
|
||||
|
||||
MTRequestContext *requestContext = [[MTRequestContext alloc] initWithMessageId:preparedMessage.messageId messageSeqNo:preparedMessage.seqNo transactionId:nil quickAckId:0];
|
||||
requestContext.sentTimestamp = CFAbsoluteTimeGetCurrent();
|
||||
requestContext.willInitializeApi = requestsWillInitializeApi;
|
||||
requestContext.waitingForMessageId = true;
|
||||
request.requestContext = requestContext;
|
||||
@ -646,6 +647,7 @@
|
||||
continue;
|
||||
}
|
||||
MTRequestContext *requestContext = [[MTRequestContext alloc] initWithMessageId:preparedMessage.messageId messageSeqNo:preparedMessage.seqNo transactionId:messageInternalIdToTransactionId[messageInternalId] quickAckId:(int32_t)[messageInternalIdToQuickAckId[messageInternalId] intValue]];
|
||||
requestContext.sentTimestamp = CFAbsoluteTimeGetCurrent();
|
||||
requestContext.willInitializeApi = requestsWillInitializeApi;
|
||||
request.requestContext = requestContext;
|
||||
}
|
||||
@ -667,7 +669,7 @@
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)mtProto:(MTProto *)__unused mtProto receivedMessage:(MTIncomingMessage *)message authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector
|
||||
- (void)mtProto:(MTProto *)__unused mtProto receivedMessage:(MTIncomingMessage *)message authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector networkType:(int32_t)networkType
|
||||
{
|
||||
if ([message.body isKindOfClass:[MTRpcResultMessage class]])
|
||||
{
|
||||
@ -871,6 +873,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
double sentTimestamp = request.requestContext.sentTimestamp;
|
||||
|
||||
request.requestContext = nil;
|
||||
|
||||
if (restartRequest)
|
||||
@ -879,11 +883,17 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
void (^completed)(id result, NSTimeInterval completionTimestamp, id error) = [request.completed copy];
|
||||
void (^completed)(id result, MTRequestResponseInfo *info, id error) = [request.completed copy];
|
||||
[_requests removeObjectAtIndex:(NSUInteger)index];
|
||||
|
||||
if (completed)
|
||||
completed(rpcResult, message.timestamp, rpcError);
|
||||
if (completed) {
|
||||
double duration = 0.0;
|
||||
if (sentTimestamp != 0.0) {
|
||||
duration = CFAbsoluteTimeGetCurrent() - sentTimestamp;
|
||||
}
|
||||
MTRequestResponseInfo *info = [[MTRequestResponseInfo alloc] initWithNetworkType:networkType timestamp:message.timestamp duration:duration];
|
||||
completed(rpcResult, info, rpcError);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
@ -121,7 +121,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)mtProto:(MTProto *)mtProto receivedMessage:(MTIncomingMessage *)message authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector
|
||||
- (void)mtProto:(MTProto *)mtProto receivedMessage:(MTIncomingMessage *)message authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector networkType:(int32_t)networkType
|
||||
{
|
||||
if (message.messageId == _messageId)
|
||||
{
|
||||
|
@ -13,7 +13,7 @@
|
||||
|
||||
- (void)tcpConnectionOpened:(MTTcpConnection *)connection;
|
||||
- (void)tcpConnectionClosed:(MTTcpConnection *)connection error:(bool)error;
|
||||
- (void)tcpConnectionReceivedData:(MTTcpConnection *)connection data:(NSData *)data;
|
||||
- (void)tcpConnectionReceivedData:(MTTcpConnection *)connection networkType:(int32_t)networkType data:(NSData *)data;
|
||||
- (void)tcpConnectionReceivedQuickAck:(MTTcpConnection *)connection quickAck:(int32_t)quickAck;
|
||||
- (void)tcpConnectionDecodePacketProgressToken:(MTTcpConnection *)connection data:(NSData *)data token:(int64_t)token completion:(void (^)(int64_t token, id packetProgressToken))completion;
|
||||
- (void)tcpConnectionProgressUpdated:(MTTcpConnection *)connection packetProgressToken:(id)packetProgressToken packetLength:(NSUInteger)packetLength progress:(float)progress;
|
||||
|
@ -664,10 +664,10 @@ struct ctr_state {
|
||||
}
|
||||
}
|
||||
|
||||
- (void)socket:(GCDAsyncSocket *)socket didReadData:(NSData *)rawData withTag:(long)tag {
|
||||
- (void)socket:(GCDAsyncSocket *)socket didReadData:(NSData *)rawData withTag:(long)tag networkType:(int32_t)networkType {
|
||||
id<MTTcpConnectionInterfaceDelegate> delegate = _delegate;
|
||||
if (delegate) {
|
||||
[delegate connectionInterfaceDidReadData:rawData withTag:tag];
|
||||
[delegate connectionInterfaceDidReadData:rawData withTag:tag networkType:networkType];
|
||||
}
|
||||
}
|
||||
|
||||
@ -717,6 +717,7 @@ struct ctr_state {
|
||||
|
||||
id<MTTcpConnectionInterface> _socket;
|
||||
bool _closed;
|
||||
int32_t _lastNetworkType;
|
||||
|
||||
bool _useIntermediateFormat;
|
||||
|
||||
@ -1459,7 +1460,7 @@ struct ctr_state {
|
||||
[_socket readDataToLength:4 withTimeout:-1 tag:MTTcpSocksRequest];
|
||||
}
|
||||
|
||||
- (void)connectionInterfaceDidReadData:(NSData *)rawData withTag:(long)tag
|
||||
- (void)connectionInterfaceDidReadData:(NSData *)rawData withTag:(long)tag networkType:(int32_t)networkType
|
||||
{
|
||||
if (_closed)
|
||||
return;
|
||||
@ -1754,12 +1755,12 @@ struct ctr_state {
|
||||
[_socket readDataToLength:(int)nextLength withTimeout:-1 tag:MTTcpSocksReceiveComplexPacketPart];
|
||||
return;
|
||||
} else if (tag == MTTcpSocksReceiveComplexPacketPart) {
|
||||
[self addReadData:rawData];
|
||||
[self addReadData:rawData networkType:networkType];
|
||||
|
||||
[_socket readDataToLength:5 withTimeout:-1 tag:MTTcpSocksReceiveComplexLength];
|
||||
return;
|
||||
} else {
|
||||
[self addReadData:rawData];
|
||||
[self addReadData:rawData networkType:networkType];
|
||||
}
|
||||
}
|
||||
|
||||
@ -1775,15 +1776,15 @@ struct ctr_state {
|
||||
[_receivedDataBuffer replaceBytesInRange:NSMakeRange(0, _pendingReceiveData.length) withBytes:nil length:0];
|
||||
int tag = _pendingReceiveData.tag;
|
||||
_pendingReceiveData = nil;
|
||||
[self processReceivedData:rawData tag:tag];
|
||||
[self processReceivedData:rawData tag:tag networkType:_lastNetworkType];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addReadData:(NSData *)data {
|
||||
- (void)addReadData:(NSData *)data networkType:(int32_t)networkType {
|
||||
if (_pendingReceiveData != nil && _pendingReceiveData.length == data.length) {
|
||||
int tag = _pendingReceiveData.tag;
|
||||
_pendingReceiveData = nil;
|
||||
[self processReceivedData:data tag:tag];
|
||||
[self processReceivedData:data tag:tag networkType:networkType];
|
||||
} else {
|
||||
[_receivedDataBuffer appendData:data];
|
||||
if (_pendingReceiveData != nil) {
|
||||
@ -1792,13 +1793,15 @@ struct ctr_state {
|
||||
[_receivedDataBuffer replaceBytesInRange:NSMakeRange(0, _pendingReceiveData.length) withBytes:nil length:0];
|
||||
int tag = _pendingReceiveData.tag;
|
||||
_pendingReceiveData = nil;
|
||||
[self processReceivedData:rawData tag:tag];
|
||||
[self processReceivedData:rawData tag:tag networkType:networkType];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)processReceivedData:(NSData *)rawData tag:(int)tag {
|
||||
- (void)processReceivedData:(NSData *)rawData tag:(int)tag networkType:(int32_t)networkType {
|
||||
_lastNetworkType = networkType;
|
||||
|
||||
NSMutableData *decryptedData = [[NSMutableData alloc] initWithLength:rawData.length];
|
||||
[_incomingAesCtr encryptIn:rawData.bytes out:decryptedData.mutableBytes len:rawData.length];
|
||||
|
||||
@ -1968,8 +1971,8 @@ struct ctr_state {
|
||||
if (_connectionReceivedData)
|
||||
_connectionReceivedData(packetData);
|
||||
id<MTTcpConnectionDelegate> delegate = _delegate;
|
||||
if ([delegate respondsToSelector:@selector(tcpConnectionReceivedData:data:)])
|
||||
[delegate tcpConnectionReceivedData:self data:packetData];
|
||||
if ([delegate respondsToSelector:@selector(tcpConnectionReceivedData:networkType:data:)])
|
||||
[delegate tcpConnectionReceivedData:self networkType:networkType data:packetData];
|
||||
}
|
||||
|
||||
if (_useIntermediateFormat) {
|
||||
|
@ -452,7 +452,7 @@ static const NSTimeInterval MTTcpTransportSleepWatchdogTimeout = 60.0;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)tcpConnectionReceivedData:(MTTcpConnection *)connection data:(NSData *)data
|
||||
- (void)tcpConnectionReceivedData:(MTTcpConnection *)connection networkType:(int32_t)networkType data:(NSData *)data
|
||||
{
|
||||
MTTcpTransportContext *transportContext = _transportContext;
|
||||
[[MTTcpTransport tcpTransportQueue] dispatchOnQueue:^
|
||||
@ -464,7 +464,7 @@ static const NSTimeInterval MTTcpTransportSleepWatchdogTimeout = 60.0;
|
||||
[self startActualizationPingResendTimer];
|
||||
|
||||
__weak MTTcpTransport *weakSelf = self;
|
||||
[self _processIncomingData:data scheme:connection.scheme transactionId:connection.internalId requestTransactionAfterProcessing:false decodeResult:^(id transactionId, bool success)
|
||||
[self _processIncomingData:data scheme:connection.scheme networkType:networkType transactionId:connection.internalId requestTransactionAfterProcessing:false decodeResult:^(id transactionId, bool success)
|
||||
{
|
||||
if (success)
|
||||
{
|
||||
@ -760,7 +760,7 @@ static const NSTimeInterval MTTcpTransportSleepWatchdogTimeout = 60.0;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)mtProto:(MTProto *)__unused mtProto receivedMessage:(MTIncomingMessage *)incomingMessage authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector
|
||||
- (void)mtProto:(MTProto *)__unused mtProto receivedMessage:(MTIncomingMessage *)incomingMessage authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector networkType:(int32_t)networkType
|
||||
{
|
||||
if ([incomingMessage.body isKindOfClass:[MTPongMessage class]])
|
||||
{
|
||||
|
@ -127,7 +127,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)mtProto:(MTProto *)mtProto receivedMessage:(MTIncomingMessage *)message authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector
|
||||
- (void)mtProto:(MTProto *)mtProto receivedMessage:(MTIncomingMessage *)message authInfoSelector:(MTDatacenterAuthInfoSelector)authInfoSelector networkType:(int32_t)networkType
|
||||
{
|
||||
if ([message.body isKindOfClass:[MTFutureSaltsMessage class]] && ((MTFutureSaltsMessage *)message.body).requestMessageId == _currentMessageId)
|
||||
{
|
||||
|
@ -58,12 +58,12 @@
|
||||
{
|
||||
}
|
||||
|
||||
- (void)_processIncomingData:(NSData *)data scheme:(MTTransportScheme *)scheme transactionId:(id)transactionId requestTransactionAfterProcessing:(bool)requestTransactionAfterProcessing decodeResult:(void (^)(id transactionId, bool success))decodeResult
|
||||
- (void)_processIncomingData:(NSData *)data scheme:(MTTransportScheme *)scheme networkType:(int32_t)networkType transactionId:(id)transactionId requestTransactionAfterProcessing:(bool)requestTransactionAfterProcessing decodeResult:(void (^)(id transactionId, bool success))decodeResult
|
||||
{
|
||||
id<MTTransportDelegate> delegate = _delegate;
|
||||
if ([delegate respondsToSelector:@selector(transportHasIncomingData:scheme:data:transactionId:requestTransactionAfterProcessing:decodeResult:)])
|
||||
if ([delegate respondsToSelector:@selector(transportHasIncomingData:scheme:networkType:data:transactionId:requestTransactionAfterProcessing:decodeResult:)])
|
||||
{
|
||||
[delegate transportHasIncomingData:self scheme:scheme data:data transactionId:transactionId requestTransactionAfterProcessing:requestTransactionAfterProcessing decodeResult:decodeResult];
|
||||
[delegate transportHasIncomingData:self scheme:scheme networkType:networkType data:data transactionId:transactionId requestTransactionAfterProcessing:requestTransactionAfterProcessing decodeResult:decodeResult];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -139,7 +139,7 @@ public final class MediaBox {
|
||||
|
||||
private let statusQueue = Queue()
|
||||
private let concurrentQueue = Queue.concurrentDefaultQueue()
|
||||
private let dataQueue = Queue()
|
||||
private let dataQueue = Queue(name: "MediaBox-Data")
|
||||
private let dataFileManager: MediaBoxFileManager
|
||||
private let cacheQueue = Queue()
|
||||
private let timeBasedCleanup: TimeBasedCleanup
|
||||
@ -209,6 +209,58 @@ public final class MediaBox {
|
||||
let _ = self.ensureDirectoryCreated
|
||||
|
||||
//self.updateResourceIndex()
|
||||
|
||||
/*#if DEBUG
|
||||
self.dataQueue.async {
|
||||
for _ in 0 ..< 5 {
|
||||
let tempFile = TempBox.shared.tempFile(fileName: "file")
|
||||
print("MediaBox test: file \(tempFile.path)")
|
||||
let queue2 = Queue.concurrentDefaultQueue()
|
||||
if let fileContext = MediaBoxFileContextV2Impl(queue: self.dataQueue, manager: self.dataFileManager, storageBox: self.storageBox, resourceId: tempFile.path.data(using: .utf8)!, path: tempFile.path + "_complete", partialPath: tempFile.path + "_partial", metaPath: tempFile.path + "_partial" + ".meta") {
|
||||
let _ = fileContext.fetched(
|
||||
range: 0 ..< Int64.max,
|
||||
priority: .default,
|
||||
fetch: { ranges in
|
||||
return ranges
|
||||
|> filter { !$0.isEmpty }
|
||||
|> take(1)
|
||||
|> castError(MediaResourceDataFetchError.self)
|
||||
|> mapToSignal { _ in
|
||||
return Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> { subscriber in
|
||||
queue2.async {
|
||||
subscriber.putNext(.resourceSizeUpdated(524288))
|
||||
}
|
||||
queue2.async {
|
||||
subscriber.putNext(.resourceSizeUpdated(393216))
|
||||
}
|
||||
queue2.async {
|
||||
subscriber.putNext(.resourceSizeUpdated(655360))
|
||||
}
|
||||
queue2.async {
|
||||
subscriber.putNext(.resourceSizeUpdated(169608))
|
||||
}
|
||||
queue2.async {
|
||||
subscriber.putNext(.dataPart(resourceOffset: 131072, data: Data(repeating: 0xbb, count: 38536), range: 0 ..< 38536, complete: true))
|
||||
}
|
||||
queue2.async {
|
||||
subscriber.putNext(.dataPart(resourceOffset: 0, data: Data(repeating: 0xaa, count: 131072), range: 0 ..< 131072, complete: false))
|
||||
}
|
||||
|
||||
return EmptyDisposable
|
||||
}
|
||||
}
|
||||
},
|
||||
error: { _ in
|
||||
},
|
||||
completed: {
|
||||
assert(try! Data(contentsOf: URL(fileURLWithPath: tempFile.path + "_complete")) == Data(repeating: 0xaa, count: 131072) + Data(repeating: 0xbb, count: 38536))
|
||||
let _ = fileContext.addReference()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif*/
|
||||
}
|
||||
|
||||
public func setMaxStoreTimes(general: Int32, shortLived: Int32, gigabytesLimit: Int32) {
|
||||
@ -688,7 +740,7 @@ public final class MediaBox {
|
||||
let clippedLowerBound = min(completeSize, max(0, range.lowerBound))
|
||||
let clippedUpperBound = min(completeSize, max(0, range.upperBound))
|
||||
if clippedLowerBound < clippedUpperBound && (clippedUpperBound - clippedLowerBound) <= 64 * 1024 * 1024 {
|
||||
file.seek(position: clippedLowerBound)
|
||||
let _ = file.seek(position: clippedLowerBound)
|
||||
let data = file.readData(count: Int(clippedUpperBound - clippedLowerBound))
|
||||
subscriber.putNext((data, true))
|
||||
} else {
|
||||
@ -725,7 +777,7 @@ public final class MediaBox {
|
||||
subscriber.putNext((Data(), true))
|
||||
subscriber.putCompletion()
|
||||
} else if clippedUpperBound <= fileSize && (clippedUpperBound - clippedLowerBound) <= 64 * 1024 * 1024 {
|
||||
file.seek(position: Int64(clippedLowerBound))
|
||||
let _ = file.seek(position: Int64(clippedLowerBound))
|
||||
let resultData = file.readData(count: Int(clippedUpperBound - clippedLowerBound))
|
||||
subscriber.putNext((resultData, true))
|
||||
subscriber.putCompletion()
|
||||
|
@ -88,7 +88,7 @@ final class MediaBoxPartialFile {
|
||||
guard let clippedRange = fileMap.contains(range) else {
|
||||
return nil
|
||||
}
|
||||
fd.seek(position: Int64(clippedRange.lowerBound))
|
||||
let _ = fd.seek(position: Int64(clippedRange.lowerBound))
|
||||
return fd.readData(count: Int(clippedRange.upperBound - clippedRange.lowerBound))
|
||||
}
|
||||
|
||||
@ -227,7 +227,7 @@ final class MediaBoxPartialFile {
|
||||
|
||||
do {
|
||||
try self.fd.access { fd in
|
||||
fd.seek(position: offset)
|
||||
let _ = fd.seek(position: offset)
|
||||
let written = data.withUnsafeBytes { rawBytes -> Int in
|
||||
let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||
|
||||
@ -330,7 +330,7 @@ final class MediaBoxPartialFile {
|
||||
do {
|
||||
var result: Data?
|
||||
try self.fd.access { fd in
|
||||
fd.seek(position: Int64(actualRange.lowerBound))
|
||||
let _ = fd.seek(position: Int64(actualRange.lowerBound))
|
||||
var data = Data(count: actualRange.count)
|
||||
let dataCount = data.count
|
||||
let readBytes = data.withUnsafeMutableBytes { rawBytes -> Int in
|
||||
|
@ -421,21 +421,28 @@ final class MediaBoxFileContextV2Impl: MediaBoxFileContext {
|
||||
private func processWrite(resourceOffset: Int64, data: Data, dataRange: Range<Int64>) {
|
||||
if let destinationFile = self.destinationFile {
|
||||
do {
|
||||
var success = true
|
||||
try destinationFile.access { fd in
|
||||
fd.seek(position: resourceOffset)
|
||||
let written = data.withUnsafeBytes { rawBytes -> Int in
|
||||
let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||
|
||||
return fd.write(bytes.advanced(by: Int(dataRange.lowerBound)), count: dataRange.count)
|
||||
if fd.seek(position: resourceOffset) {
|
||||
let written = data.withUnsafeBytes { rawBytes -> Int in
|
||||
let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
|
||||
|
||||
return fd.write(bytes.advanced(by: Int(dataRange.lowerBound)), count: dataRange.count)
|
||||
}
|
||||
assert(written == dataRange.count)
|
||||
} else {
|
||||
success = false
|
||||
}
|
||||
assert(written == dataRange.count)
|
||||
}
|
||||
|
||||
let range: Range<Int64> = resourceOffset ..< (resourceOffset + Int64(dataRange.count))
|
||||
self.fileMap.fill(range)
|
||||
self.fileMap.serialize(manager: self.manager, to: self.metaPath)
|
||||
|
||||
self.storageBox.update(id: self.resourceId, size: self.fileMap.sum)
|
||||
if success {
|
||||
let range: Range<Int64> = resourceOffset ..< (resourceOffset + Int64(dataRange.count))
|
||||
self.fileMap.fill(range)
|
||||
self.fileMap.serialize(manager: self.manager, to: self.metaPath)
|
||||
|
||||
self.storageBox.update(id: self.resourceId, size: self.fileMap.sum)
|
||||
} else {
|
||||
postboxLog("MediaBoxFileContextV2Impl: error seeking file to \(resourceOffset) at \(self.partialPath)")
|
||||
}
|
||||
} catch let e {
|
||||
postboxLog("MediaBoxFileContextV2Impl: error writing file at \(self.partialPath): \(e)")
|
||||
}
|
||||
|
@ -32,8 +32,8 @@ final class MediaBoxFileManager {
|
||||
return self.file.readData(count: count)
|
||||
}
|
||||
|
||||
func seek(position: Int64) {
|
||||
self.file.seek(position: position)
|
||||
func seek(position: Int64) -> Bool {
|
||||
return self.file.seek(position: position)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -203,7 +203,7 @@ final class MediaBoxFileMap {
|
||||
}
|
||||
|
||||
let _ = try? fileItem.access { file in
|
||||
file.seek(position: 0)
|
||||
let _ = file.seek(position: 0)
|
||||
let buffer = WriteBuffer()
|
||||
var magic: UInt32 = 0x7bac1487
|
||||
buffer.write(&magic, offset: 0, length: 4)
|
||||
|
@ -645,7 +645,7 @@ private final class PremiumGiftScreenComponent: CombinedComponent {
|
||||
price = nil
|
||||
}
|
||||
let buttonText = presentationData.strings.Premium_Gift_GiftSubscription(price ?? "—").string
|
||||
self.buttonStatePromise.set(.single(AttachmentMainButtonState(text: buttonText, background: .premium, textColor: .white, isVisible: true, progress: self.inProgress ? .center : .none, isEnabled: true)))
|
||||
self.buttonStatePromise.set(.single(AttachmentMainButtonState(text: buttonText, font: .bold, background: .premium, textColor: .white, isVisible: true, progress: self.inProgress ? .center : .none, isEnabled: true)))
|
||||
}
|
||||
|
||||
func buy() {
|
||||
|
@ -606,14 +606,19 @@ public final class PremiumLimitDisplayComponent: CombinedComponent {
|
||||
|
||||
let activityPosition = floor(context.availableSize.width * component.badgeGraphPosition)
|
||||
|
||||
var inactiveTitleOpacity: CGFloat = 1.0
|
||||
var inactiveValueOpacity: CGFloat = 1.0
|
||||
if inactiveValue.size.width + inactiveTitle.size.width >= activityPosition - 8.0 {
|
||||
inactiveValueOpacity = 0.0
|
||||
|
||||
if 12.0 + inactiveValue.size.width + 4.0 + inactiveTitle.size.width + 12.0 >= activityPosition - 8.0 {
|
||||
inactiveTitleOpacity = 0.0
|
||||
if 12.0 + inactiveValue.size.width + 12.0 >= activityPosition - 8.0 {
|
||||
inactiveValueOpacity = 0.0
|
||||
}
|
||||
}
|
||||
|
||||
context.add(inactiveTitle
|
||||
.position(CGPoint(x: inactiveTitle.size.width / 2.0 + 12.0, y: height - lineHeight / 2.0))
|
||||
.opacity(inactiveValueOpacity)
|
||||
.opacity(inactiveTitleOpacity)
|
||||
)
|
||||
|
||||
context.add(inactiveValue
|
||||
|
@ -239,9 +239,8 @@ public final class QrCodeScreen: ViewController {
|
||||
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 add this folder and join the chats included in this invite link."
|
||||
title = self.presentationData.strings.InviteLink_QRCodeFolder_Title
|
||||
text = self.presentationData.strings.InviteLink_QRCodeFolder_Text
|
||||
default:
|
||||
title = ""
|
||||
text = ""
|
||||
|
@ -112,6 +112,7 @@ swift_library(
|
||||
"//submodules/FeaturedStickersScreen:FeaturedStickersScreen",
|
||||
"//submodules/MediaPickerUI:MediaPickerUI",
|
||||
"//submodules/ImageBlur:ImageBlur",
|
||||
"//submodules/AttachmentUI:AttachmentUI",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -669,12 +669,8 @@ private func selectivePrivacySettingsControllerEntries(presentationData: Present
|
||||
|
||||
entries.append(.everybody(presentationData.theme, presentationData.strings.PrivacySettings_LastSeenEverybody, state.setting == .everybody))
|
||||
entries.append(.contacts(presentationData.theme, presentationData.strings.PrivacySettings_LastSeenContacts, state.setting == .contacts))
|
||||
switch kind {
|
||||
case .presence, .voiceCalls, .forwards, .phoneNumber, .voiceMessages, .profilePhoto:
|
||||
entries.append(.nobody(presentationData.theme, presentationData.strings.PrivacySettings_LastSeenNobody, state.setting == .nobody))
|
||||
case .groupInvitations:
|
||||
break
|
||||
}
|
||||
entries.append(.nobody(presentationData.theme, presentationData.strings.PrivacySettings_LastSeenNobody, state.setting == .nobody))
|
||||
|
||||
let phoneLink = "https://t.me/+\(phoneNumber)"
|
||||
if let settingInfoText = settingInfoText {
|
||||
entries.append(.settingInfo(presentationData.theme, settingInfoText, phoneLink))
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user