mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Merge branch 'custom-webrtc-build' into experimental-2
This commit is contained in:
commit
faf66c9b4b
837 changed files with 180761 additions and 7926 deletions
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -37,4 +37,4 @@ Remove, if not applicable
|
|||
|
||||
**iOS version**: `13.X`
|
||||
|
||||
**App version:** `5.X`
|
||||
**App version:** `7.X`
|
||||
|
|
|
|||
2
Makefile
2
Makefile
|
|
@ -3,7 +3,7 @@
|
|||
include Utils.makefile
|
||||
|
||||
|
||||
APP_VERSION="6.3"
|
||||
APP_VERSION="7.0"
|
||||
CORE_COUNT=$(shell sysctl -n hw.logicalcpu)
|
||||
CORE_COUNT_MINUS_ONE=$(shell expr ${CORE_COUNT} \- 1)
|
||||
|
||||
|
|
|
|||
1
Random.txt
Normal file
1
Random.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
LS3VfNVetXNy6mHmek8hegOh0wsvmJs0hcrc7PLA9eI=
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
@implementation Serialization
|
||||
|
||||
- (NSUInteger)currentLayer {
|
||||
return 116;
|
||||
return 117;
|
||||
}
|
||||
|
||||
- (id _Nullable)parseMessage:(NSData * _Nullable)data {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"NSLocationWhenInUseUsageDescription" = "Si envieu la vostra ubicació als amics, Telegram requereix accés per a mostra-los un mapa.";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "Si trieu de compartir la vostra ubicació en directe amb amics en un xat, Telegram requereix accés en segon pla a la vostra ubicació per a actualitzar-la durant la compartició en directe.";
|
||||
"NSLocationAlwaysUsageDescription" = "Si trieu de compartir la vostra ubicació en directe amb amics en un xat, Telegram requereix de tenir accés en segon pla a la vostra ubicació per a actualitzar-la durant la compartició en directe. També necessiteu això per a enviar ubicacions des d'un Apple Watch.";
|
||||
"NSCameraUsageDescription" = "Ens cal això perquè pugueu fer i compartir fotos i vídeos.";
|
||||
"NSCameraUsageDescription" = "Ens cal això perquè pugueu fer i compartir fotos i vídeos, així com fer videotrucades.";
|
||||
"NSPhotoLibraryUsageDescription" = "Ens cal això perquè pugueu compartir fotos i vídeos de la biblioteca de fotos.";
|
||||
"NSPhotoLibraryAddUsageDescription" = "Ens cal això perquè així pugueu desar fotos i vídeos a la biblioteca de fotos.";
|
||||
"NSMicrophoneUsageDescription" = "Ens cal això perquè pugueu enregistrar i compartir missatges de veu i vídeos amb so.";
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"NSLocationWhenInUseUsageDescription" = "When you send your location to your friends, Telegram needs access to show them a map.";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "When you choose to share your Live Location with friends in a chat, Telegram needs background access to your location to keep them updated for the duration of the live sharing.";
|
||||
"NSLocationAlwaysUsageDescription" = "When you choose to share your live location with friends in a chat, Telegram needs background access to your location to keep them updated for the duration of the live sharing. You also need this to send locations from an Apple Watch.";
|
||||
"NSCameraUsageDescription" = "We need this so that you can take and share photos and videos.";
|
||||
"NSCameraUsageDescription" = "We need this so that you can take and share photos and videos, as well as make video calls.";
|
||||
"NSPhotoLibraryUsageDescription" = "We need this so that you can share photos and videos from your photo library.";
|
||||
"NSPhotoLibraryAddUsageDescription" = "We need this so that you can save photos and videos to your photo library.";
|
||||
"NSMicrophoneUsageDescription" = "We need this so that you can record and share voice messages and videos with sound.";
|
||||
|
|
|
|||
|
|
@ -215,7 +215,9 @@
|
|||
"PUSH_AUTH_REGION" = "New login|from unrecognized device %1$@, location: %2$@";
|
||||
|
||||
"PUSH_PHONE_CALL_REQUEST" = "%1$@|is calling you!";
|
||||
"PUSH_VIDEO_CALL_REQUEST" = "%1$@|is calling you!";
|
||||
"PUSH_PHONE_CALL_MISSED" = "%1$@|You missed a call";
|
||||
"PUSH_VIDEO_CALL_MISSED" = "%1$@|You missed a video call";
|
||||
|
||||
"PUSH_MESSAGE_GAME_SCORE" = "%1$@ scored %3$@ in game %2$@";
|
||||
"PUSH_MESSAGE_VIDEOS" = "%1$@ sent you %2$@ videos";
|
||||
|
|
@ -2293,7 +2295,9 @@ Unused sets are archived when you add more.";
|
|||
"Notification.CallIncoming" = "Incoming Call";
|
||||
"Notification.VideoCallIncoming" = "Incoming Video Call";
|
||||
"Notification.CallMissed" = "Missed Call";
|
||||
"Notification.VideoCallMissed" = "Missed Video Call";
|
||||
"Notification.CallCanceled" = "Cancelled Call";
|
||||
"Notification.VideoCallCanceled" = "Cancelled Video Call";
|
||||
"Notification.CallOutgoingShort" = "Outgoing";
|
||||
"Notification.CallIncomingShort" = "Incoming";
|
||||
"Notification.CallMissedShort" = "Missed";
|
||||
|
|
@ -2470,6 +2474,7 @@ Unused sets are archived when you add more.";
|
|||
|
||||
"Call.CallInProgressTitle" = "Call in Progress";
|
||||
"Call.CallInProgressMessage" = "Finish call with %1$@ and start a new one with %2$@?";
|
||||
"Call.ExternalCallInProgressMessage" = "Please finish the current call first.";
|
||||
|
||||
"Call.Message" = "Message";
|
||||
|
||||
|
|
@ -3023,7 +3028,7 @@ Unused sets are archived when you add more.";
|
|||
|
||||
"InfoPlist.NSContactsUsageDescription" = "Telegram will continuously upload your contacts to its heavily encrypted cloud servers to let you connect with your friends across all your devices.";
|
||||
"InfoPlist.NSLocationWhenInUseUsageDescription" = "When you send your location to your friends, Telegram needs access to show them a map.";
|
||||
"InfoPlist.NSCameraUsageDescription" = "We need this so that you can take and share photos and videos.";
|
||||
"InfoPlist.NSCameraUsageDescription" = "We need this so that you can take and share photos and videos, as well as make video calls.";
|
||||
"InfoPlist.NSPhotoLibraryUsageDescription" = "We need this so that you can share photos and videos from your photo library.";
|
||||
"InfoPlist.NSPhotoLibraryAddUsageDescription" = "We need this so that you can save photos and videos to your photo library.";
|
||||
"InfoPlist.NSMicrophoneUsageDescription" = "We need this so that you can record and share voice messages and videos with sound.";
|
||||
|
|
@ -4788,6 +4793,7 @@ Any member of this group will be able to see messages in the channel.";
|
|||
"ContactList.Context.SendMessage" = "Send Message";
|
||||
"ContactList.Context.StartSecretChat" = "Start Secret Chat";
|
||||
"ContactList.Context.Call" = "Call";
|
||||
"ContactList.Context.VideoCall" = "Video Call";
|
||||
|
||||
"Theme.Context.Apply" = "Apply";
|
||||
|
||||
|
|
@ -5332,7 +5338,7 @@ Any member of this group will be able to see messages in the channel.";
|
|||
"PeerInfo.ButtonMessage" = "Message";
|
||||
"PeerInfo.ButtonDiscuss" = "Discuss";
|
||||
"PeerInfo.ButtonCall" = "Call";
|
||||
"PeerInfo.ButtonVideoCall" = "Video Call";
|
||||
"PeerInfo.ButtonVideoCall" = "Video";
|
||||
"PeerInfo.ButtonMute" = "Mute";
|
||||
"PeerInfo.ButtonUnmute" = "Unmute";
|
||||
"PeerInfo.ButtonMore" = "More";
|
||||
|
|
@ -5633,7 +5639,7 @@ Any member of this group will be able to see messages in the channel.";
|
|||
"PrivacySettings.AutoArchive" = "Archive and Mute";
|
||||
"PrivacySettings.AutoArchiveInfo" = "Automatically archive and mute new chats, groups and channels from non-contacts.";
|
||||
|
||||
"Call.RemoteVideoPaused" = "%@'s video paused";
|
||||
"Call.RemoteVideoPaused" = "%@'s video is paused";
|
||||
|
||||
"Settings.SetProfilePhotoOrVideo" = "Set Photo or Video";
|
||||
"Settings.SetNewProfilePhotoOrVideo" = "Set New Photo or Video";
|
||||
|
|
@ -5696,3 +5702,36 @@ Any member of this group will be able to see messages in the channel.";
|
|||
"EditProfile.NameAndPhotoOrVideoHelp" = "Enter your name and add an optional profile photo or video.";
|
||||
|
||||
"Settings.RemoveConfirmation" = "Remove";
|
||||
|
||||
"Conversation.ContextMenuOpenProfile" = "Open Profile";
|
||||
"Conversation.ContextMenuSendMessage" = "Send Message";
|
||||
"Conversation.ContextMenuMention" = "Mention";
|
||||
|
||||
"Conversation.ContextMenuOpenChannelProfile" = "Open Profile";
|
||||
"Conversation.ContextMenuOpenChannel" = "Open Channel";
|
||||
|
||||
"Cache.KeepMediaHelp" = "Photos, videos and other files from cloud chats that you have **not accessed** during this period will be removed from this device to save disk space.";
|
||||
|
||||
|
||||
"Cache.MaximumCacheSize" = "Maximum Cache Size";
|
||||
"Cache.NoLimit" = "No Limit";
|
||||
"Cache.MaximumCacheSizeHelp" = "If your cache size exceeds this limit, the oldest media will be deleted.\n\nAll media will stay in the Telegram cloud and can be re-downloaded if you need it again.";
|
||||
|
||||
"Stats.MessageTitle" = "Message Statistics";
|
||||
"Stats.MessageOverview" = "Overview";
|
||||
"Stats.MessageInteractionsTitle" = "Interactions";
|
||||
"Stats.MessagePublicForwardsTitle" = "Public Shares";
|
||||
|
||||
"Call.CameraTooltip" = "Tap here to turn on your camera";
|
||||
"Call.CameraConfirmationText" = "Switch to video call?";
|
||||
"Call.CameraConfirmationConfirm" = "Switch";
|
||||
|
||||
"Call.YourMicrophoneOff" = "Your microphone is off";
|
||||
"Call.MicrophoneOff" = "%@'s microphone is off";
|
||||
"Call.CameraOff" = "%@'s camera is off";
|
||||
"Call.BatteryLow" = "%@'s battery level is low";
|
||||
|
||||
"Call.Audio" = "audio";
|
||||
"Call.AudioRouteMute" = "Mute Yourself";
|
||||
|
||||
"AccessDenied.VideoCallCamera" = "Telegram needs access to your camera to make video calls.\n\nPlease go to Settings > Privacy > Camera and set Telegram to ON.";
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"NSLocationWhenInUseUsageDescription" = "Cuando envías tu ubicación a tus amigos, Telegram necesita acceso para mostrarles un mapa.";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "Cuando eliges compartir tu ubicación en tiempo real con amigos en un chat, Telegram necesita acceso en segundo plano a tu ubicación para mantenerla actualizada mientras la función esté en uso.";
|
||||
"NSLocationAlwaysUsageDescription" = "Cuando envías tu ubicación a tus amigos, Telegram necesita acceso para mostrarles un mapa. También es requerido para enviar ubicaciones desde un Apple Watch.";
|
||||
"NSCameraUsageDescription" = "Es requerido para que puedas hacer fotos y vídeos.";
|
||||
"NSCameraUsageDescription" = "Necesitamos esto para que puedas tomar y compartir fotos y videos, así como para realizar videollamadas.";
|
||||
"NSPhotoLibraryUsageDescription" = "Es requerido para que puedas compartir fotos y vídeos desde tu biblioteca de fotos.";
|
||||
"NSPhotoLibraryAddUsageDescription" = "Necesitamos esto para que puedas guardar fotos y videos en tu biblioteca de fotos.";
|
||||
"NSMicrophoneUsageDescription" = "Es requerido para que puedas grabar y compartir mensajes de voz y vídeos con sonido.";
|
||||
|
|
|
|||
17
Telegram/Telegram-iOS/fa.lproj/AppIntentVocabulary.plist
Normal file
17
Telegram/Telegram-iOS/fa.lproj/AppIntentVocabulary.plist
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IntentPhrases</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>IntentName</key>
|
||||
<string>INSendMessageIntent</string>
|
||||
<key>IntentExamples</key>
|
||||
<array>
|
||||
<string>Send a Telegram message to Alex saying I'll be there in 10 minutes</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
12
Telegram/Telegram-iOS/fa.lproj/InfoPlist.strings
Normal file
12
Telegram/Telegram-iOS/fa.lproj/InfoPlist.strings
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/* Localized versions of Info.plist keys */
|
||||
|
||||
"NSContactsUsageDescription" = "تلگرام به طور مداوم مخاطبین شما را به سرورهای اَبری و رمزگذاری شده خود آپلود میکند تا شما بتوانید در تمامی دستگاههای خود با دوستانتان ارتباط برقرار کنید.";
|
||||
"NSLocationWhenInUseUsageDescription" = "هنگامی که شما موقعیت مکانی خود را برای دوستانتان ارسال میکنید، تلگرام جهت نشان دادن نقشه به آنها نیازمند دسترسی است.";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "در صورتی که تصمیم بگیرید موقعیت مکانی لحظهای خود را در یک گفتگو با دوستانتان به اشتراک بگذارید، تلگرام جهت بهروزرسانی این اطلاعات در طول به اشتراک گذاری لحظهای، باید در پسزمینه به موقعیت مکانی شما دسترسی داشته باشد.";
|
||||
"NSLocationAlwaysUsageDescription" = "در صورتی که شما تصمیم بگیرید موقعیت مکانی لحظهای خود را در یک گفتگو با دوستانتان به اشتراک بگذارید، تلگرام جهت بهروزرسانی این اطلاعات در طول به اشتراک گذاری لحظهای، باید در پسزمینه به موقعیت مکانی شما دسترسی داشته باشد. شما همچنین برای ارسال موقعیت مکانی از ساعت اَپل به این دسترسی نیاز دارید.";
|
||||
"NSCameraUsageDescription" = "ما به این دسترسی نیاز داریم تا شما بتوانید عکس و ویدیو گرفته و آنها را به اشتراک بگذارید.";
|
||||
"NSPhotoLibraryUsageDescription" = "ما به این دسترسی نیاز داریم تا شما بتوانید از گالری تصاویر خود عکس و ویدیو به اشتراک بگذارید.";
|
||||
"NSPhotoLibraryAddUsageDescription" = "ما به این دسترسی نیاز داریم تا شما بتوانید عکسها و ویدیوها را در گالری تصاویر خود ذخیره کنید.";
|
||||
"NSMicrophoneUsageDescription" = "ما به این دسترسی نیاز داریم تا شما بتوانید پیامهای صوتی و ویدیوهای صدادار را ضبط کرده و به اشتراک بگذارید.";
|
||||
"NSSiriUsageDescription" = "شما میتوانید با استفاده از Siri پیام ارسال کنید.";
|
||||
"NSFaceIDUsageDescription" = "شما میتوانید با استفاده از Face ID قفل برنامه را باز کنید.";
|
||||
0
Telegram/Telegram-iOS/fa.lproj/Localizable.strings
Normal file
0
Telegram/Telegram-iOS/fa.lproj/Localizable.strings
Normal file
|
|
@ -4,7 +4,7 @@
|
|||
"NSLocationWhenInUseUsageDescription" = "Quando invii la tua posizione ai tuoi amici, Telegram ha bisogno di accedere per mostrare loro la mappa.";
|
||||
"NSLocationAlwaysUsageDescription" = "Quando invii la tua posizione ai tuoi amici, Telegram ha bisogno di accedere per mostrare loro la mappa. Ti serve anche per inviare posizioni da Apple Watch.";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "Quando scegli di condividere la tua Posizione Attuale con gli amici in una chat, Telegram ha bisogno dell'accesso in background alla tua posizione per tenerli aggiornati durante la durata della condivisione della posizione.";
|
||||
"NSCameraUsageDescription" = "Ci serve per farti scattare, registrare e condividere foto e video.";
|
||||
"NSCameraUsageDescription" = "Ci serve per farti scattare, registrare e condividere foto e video, oltre che per fare videochiamate.";
|
||||
"NSPhotoLibraryUsageDescription" = "Ci serve per farti condividere foto e video dalla tua libreria foto.";
|
||||
"NSPhotoLibraryAddUsageDescription" = "Ci serve per farti salvare foto e video nella tua libreria foto.";
|
||||
"NSMicrophoneUsageDescription" = "Ci serve per farti registrare e condividere messaggi vocali e video con il sonoro.";
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"NSLocationWhenInUseUsageDescription" = "Bila anda hantar lokasi anda kepada rakan anda, Telegram perlukan akses untuk tunjuk peta.";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "Bila anda pilih untuk kongsi Lokasi Langsung anda dengan rakan dalam chat, Telegram perlu akses latar belakang ke lokasi anda agar lokasi anda sentiasa dikemaskini ketika perkongsian.";
|
||||
"NSLocationAlwaysUsageDescription" = "Bila anda pilih untuk kongsi lokasi langsung anda dengan rakan dalam chat, Telegram perlu akses latar belakang agar lokasi anda sentiasa dikemaskini. Anda juga harus hantar lokasi anda ke Jam Apple.";
|
||||
"NSCameraUsageDescription" = "Kita perlukan ini agar anda boleh ambil dan kongsi foto dan video.";
|
||||
"NSCameraUsageDescription" = "Kita perlukan ini agar anda boleh ambil dan kongsi foto dan video, dan juga buat panggilan video.";
|
||||
"NSPhotoLibraryUsageDescription" = "Kita perlu ini agar anda boleh kongsi foto dan video dari librari foto anda.";
|
||||
"NSPhotoLibraryAddUsageDescription" = "Kita perlu ini agar anda boleh simpan foto dan video ke librari foto anda.";
|
||||
"NSMicrophoneUsageDescription" = "Kita perlu ini agar anda boleh rekod dan kongsi mesej suara dan video dengan suara.";
|
||||
|
|
|
|||
17
Telegram/Telegram-iOS/pl.lproj/AppIntentVocabulary.plist
Normal file
17
Telegram/Telegram-iOS/pl.lproj/AppIntentVocabulary.plist
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IntentPhrases</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>IntentName</key>
|
||||
<string>INSendMessageIntent</string>
|
||||
<key>IntentExamples</key>
|
||||
<array>
|
||||
<string>Send a Telegram message to Alex saying I'll be there in 10 minutes</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
12
Telegram/Telegram-iOS/pl.lproj/InfoPlist.strings
Normal file
12
Telegram/Telegram-iOS/pl.lproj/InfoPlist.strings
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/* Localized versions of Info.plist keys */
|
||||
|
||||
"NSContactsUsageDescription" = "Telegram będzie nieprzerwanie przesyłać kontakty do silnie zaszyfrowanych serwerów w chmurze, aby umożliwić ci połączenie się ze znajomymi na wszystkich urządzeniach.";
|
||||
"NSLocationWhenInUseUsageDescription" = "Gdy wysyłasz swoją lokalizację znajomym, Telegram potrzebuje dostępu, aby pokazać im mapę.";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "Gdy zdecydujesz się udostępnić swoją „lokalizację na żywo” znajomym podczas czatu, Telegram potrzebuje dostępu w tle do twojej lokalizacji, aby zapewnić jej aktualizację przez cały czas udostępniania „na żywo”.";
|
||||
"NSLocationAlwaysUsageDescription" = "Gdy zdecydujesz się udostępnić swoją „lokalizację na żywo” znajomym podczas czatu, Telegram potrzebuje dostępu w tle do twojej lokalizacji, aby zapewnić jej aktualizację przez cały czas udostępniania „na żywo”. Jest to również potrzebne do wysyłania lokalizacji z Apple Watch.";
|
||||
"NSCameraUsageDescription" = "Potrzebujemy tego, aby można było robić i udostępniać zdjęcia i wideo, a także prowadzić rozmowy wideo.";
|
||||
"NSPhotoLibraryUsageDescription" = "Potrzebujemy tego, aby można było udostępniać zdjęcia i wideo ze swojej biblioteki zdjęć.";
|
||||
"NSPhotoLibraryAddUsageDescription" = "Potrzebujemy tego, aby można było zapisywać zdjęcia i wideo w swojej bibliotece zdjęć.";
|
||||
"NSMicrophoneUsageDescription" = "Potrzebujemy tego, aby można było nagrywać i udostępniać wiadomości głosowe i wideo z dźwiękiem.";
|
||||
"NSSiriUsageDescription" = "Możesz użyć Siri do wysyłania wiadomości.";
|
||||
"NSFaceIDUsageDescription" = "Możesz użyć Face ID, aby odblokować aplikację.";
|
||||
0
Telegram/Telegram-iOS/pl.lproj/Localizable.strings
Normal file
0
Telegram/Telegram-iOS/pl.lproj/Localizable.strings
Normal file
|
|
@ -4,7 +4,7 @@
|
|||
"NSLocationWhenInUseUsageDescription" = "Konumunuzu arkadaşlarınıza gönderdiğinizde, Telegram'ın onlara bir harita göstermesi için erişmesi gerekiyor.";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "Canlı Konumunuzu arkadaşlarınızla bir sohbette paylaşmayı seçtiğinizde, Telegram'ın canlı paylaşım süresince onları güncel tutmak için konumunuza arka plan erişimi olması gerekir.";
|
||||
"NSLocationAlwaysUsageDescription" = "Canlı konumunuzu bir sohbette arkadaşlarınızla paylaşmayı seçtiğinizde, Telegram'ın canlı paylaşım süresince konumunuzu güncel tutması için bir arka plan erişimi gerekir. Ayrıca Apple Watch'dan konum göndermek için de buna ihtiyacınız var.";
|
||||
"NSCameraUsageDescription" = "Fotoğraf ve video çekip paylaşabilmeniz için buna ihtiyacımız var.";
|
||||
"NSCameraUsageDescription" = "Fotoğraf ve video çekip paylaşabilmeniz ve görüntülü arama yapabilmeniz için buna ihtiyacımız var.";
|
||||
"NSPhotoLibraryUsageDescription" = "Fotoğraf arşivinizdeki fotoğraf ve videoları paylaşabilmeniz için buna ihtiyacımız var.";
|
||||
"NSPhotoLibraryAddUsageDescription" = "Fotoğraf arşivine fotoğraf ve video kaydedebilmeniz için buna ihtiyacımız var.";
|
||||
"NSMicrophoneUsageDescription" = "Sesli mesajları ve videoları ses ile kaydedebilmeniz ve paylaşabilmeniz için buna ihtiyacımız var.";
|
||||
|
|
|
|||
17
Telegram/Telegram-iOS/uz.lproj/AppIntentVocabulary.plist
Normal file
17
Telegram/Telegram-iOS/uz.lproj/AppIntentVocabulary.plist
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IntentPhrases</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>IntentName</key>
|
||||
<string>INSendMessageIntent</string>
|
||||
<key>IntentExamples</key>
|
||||
<array>
|
||||
<string>Send a Telegram message to Alex saying I'll be there in 10 minutes</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
12
Telegram/Telegram-iOS/uz.lproj/InfoPlist.strings
Normal file
12
Telegram/Telegram-iOS/uz.lproj/InfoPlist.strings
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/* Localized versions of Info.plist keys */
|
||||
|
||||
"NSContactsUsageDescription" = "Telegram barcha qurilmalaringizdan doʻstlaringiz bilan bogʻlana olishingiz uchun muntazam ravishda kontaktlaringizni kuchli shifrlanadigan bulut serverlariga yuklaydi.";
|
||||
"NSLocationWhenInUseUsageDescription" = "Joylashuvingizni doʻstlaringizga yuborganingizda Telegram ularga xaritani koʻrsatishi uchun ruxsat kerak boʻladi.";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "Chatda doʻstlarga Jonli joylashuvingizni ulashishni tanlaganingizda, Telegram joylashuvingizni jonli ulashish davomida yangilab turishi uchun undan fonda foydalanishi kerak.";
|
||||
"NSLocationAlwaysUsageDescription" = "Chatda doʻstlarga Jonli joylashuvingizni ulashishni tanlaganingizda, Telegram joylashuvingizni jonli ulashish davomida yangilab turishi uchun undan fonda foydalanishi kerak. Sizga bu Apple Watchdan joylashuvlarni yuborish uchun ham kerak.";
|
||||
"NSCameraUsageDescription" = "Bu bizga rasm va videolarga olish, shuningdek, video chaqiruvlar qilish va ularni ulashishingiz uchun kerak.";
|
||||
"NSPhotoLibraryUsageDescription" = "Bu bizga galereyangizdan rasm va videolaringizni ulasha olishingiz uchun kerak.";
|
||||
"NSPhotoLibraryAddUsageDescription" = "Bu bizga rasm va videolaringizni galereyangizga saqlay olishingiz uchun kerak.";
|
||||
"NSMicrophoneUsageDescription" = "Bu bizga ovozli xabarlarni tovush bilan yozishingiz va ulashingiz uchun kerak.";
|
||||
"NSSiriUsageDescription" = "Siz xabarlar yuborish uchun Siridan foydalanishingiz mumkin.";
|
||||
"NSFaceIDUsageDescription" = "Siz ilovani Face ID yordamida ochishingiz mumkin.";
|
||||
0
Telegram/Telegram-iOS/uz.lproj/Localizable.strings
Normal file
0
Telegram/Telegram-iOS/uz.lproj/Localizable.strings
Normal file
|
|
@ -1 +0,0 @@
|
|||
/private/var/tmp/_bazel_ilya/b5422c0ab62ebf818a9d47ff552063a2/execroot/__main__
|
||||
|
|
@ -17,11 +17,14 @@ fi
|
|||
XCODE_VERSION=$(cat "build-system/xcode_version")
|
||||
INSTALLED_XCODE_VERSION=$(echo `plutil -p \`xcode-select -p\`/../Info.plist | grep -e CFBundleShortVersionString | sed 's/[^0-9\.]*//g'`)
|
||||
|
||||
if [ "$INSTALLED_XCODE_VERSION" != "$XCODE_VERSION" ]; then
|
||||
echo "Xcode $XCODE_VERSION required, $INSTALLED_XCODE_VERSION installed (at $(xcode-select -p))"
|
||||
exit 1
|
||||
if [ "$IGNORE_XCODE_VERSION_MISMATCH" = "1" ]; then
|
||||
XCODE_VERSION="$INSTALLED_XCODE_VERSION"
|
||||
else
|
||||
if [ "$INSTALLED_XCODE_VERSION" != "$XCODE_VERSION" ]; then
|
||||
echo "Xcode $XCODE_VERSION required, $INSTALLED_XCODE_VERSION installed (at $(xcode-select -p))"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
GEN_DIRECTORY="build-input/gen/project"
|
||||
mkdir -p "$GEN_DIRECTORY"
|
||||
|
||||
|
|
|
|||
|
|
@ -394,6 +394,12 @@ public enum ContactListPeerId: Hashable {
|
|||
case deviceContact(DeviceContactStableId)
|
||||
}
|
||||
|
||||
public enum ContactListAction: Equatable {
|
||||
case generic
|
||||
case voiceCall
|
||||
case videoCall
|
||||
}
|
||||
|
||||
public enum ContactListPeer: Equatable {
|
||||
case peer(peer: Peer, isGlobal: Bool, participantCount: Int32?)
|
||||
case deviceContact(DeviceContactStableId, DeviceContactBasicData)
|
||||
|
|
@ -440,14 +446,16 @@ public final class ContactSelectionControllerParams {
|
|||
public let title: (PresentationStrings) -> String
|
||||
public let options: [ContactListAdditionalOption]
|
||||
public let displayDeviceContacts: Bool
|
||||
public let displayCallIcons: Bool
|
||||
public let confirmation: (ContactListPeer) -> Signal<Bool, NoError>
|
||||
|
||||
public init(context: AccountContext, autoDismiss: Bool = true, title: @escaping (PresentationStrings) -> String, options: [ContactListAdditionalOption] = [], displayDeviceContacts: Bool = false, confirmation: @escaping (ContactListPeer) -> Signal<Bool, NoError> = { _ in .single(true) }) {
|
||||
public init(context: AccountContext, autoDismiss: Bool = true, title: @escaping (PresentationStrings) -> String, options: [ContactListAdditionalOption] = [], displayDeviceContacts: Bool = false, displayCallIcons: Bool = false, confirmation: @escaping (ContactListPeer) -> Signal<Bool, NoError> = { _ in .single(true) }) {
|
||||
self.context = context
|
||||
self.autoDismiss = autoDismiss
|
||||
self.title = title
|
||||
self.options = options
|
||||
self.displayDeviceContacts = displayDeviceContacts
|
||||
self.displayCallIcons = displayCallIcons
|
||||
self.confirmation = confirmation
|
||||
}
|
||||
}
|
||||
|
|
@ -647,6 +655,7 @@ public protocol AccountContext: class {
|
|||
|
||||
var currentLimitsConfiguration: Atomic<LimitsConfiguration> { get }
|
||||
var currentContentSettings: Atomic<ContentSettings> { get }
|
||||
var currentAppConfiguration: Atomic<AppConfiguration> { get }
|
||||
|
||||
func storeSecureIdPassword(password: String)
|
||||
func getStoredSecureIdPassword() -> String?
|
||||
|
|
|
|||
|
|
@ -10,5 +10,5 @@ public protocol ChatListController: ViewController {
|
|||
func activateSearch()
|
||||
func deactivateSearch(animated: Bool)
|
||||
func activateCompose()
|
||||
func maybeAskForPeerChatRemoval(peer: RenderedPeer, deleteGloballyIfPossible: Bool, completion: @escaping (Bool) -> Void, removed: @escaping () -> Void)
|
||||
func maybeAskForPeerChatRemoval(peer: RenderedPeer, joined: Bool, deleteGloballyIfPossible: Bool, completion: @escaping (Bool) -> Void, removed: @escaping () -> Void)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import Display
|
|||
import SwiftSignalKit
|
||||
|
||||
public protocol ContactSelectionController: ViewController {
|
||||
var result: Signal<ContactListPeer?, NoError> { get }
|
||||
var result: Signal<(ContactListPeer, ContactListAction)?, NoError> { get }
|
||||
var displayProgress: Bool { get set }
|
||||
var dismissed: (() -> Void)? { get set }
|
||||
|
||||
|
|
|
|||
|
|
@ -47,13 +47,19 @@ public func messageMediaFileCancelInteractiveFetch(context: AccountContext, mess
|
|||
context.fetchManager.cancelInteractiveFetches(category: fetchCategoryForFile(file), location: .chat(messageId.peerId), locationKey: .messageId(messageId), resource: file.resource)
|
||||
}
|
||||
|
||||
public func messageMediaImageInteractiveFetched(context: AccountContext, message: Message, image: TelegramMediaImage, resource: MediaResource, storeToDownloadsPeerType: MediaAutoDownloadPeerType?) -> Signal<Void, NoError> {
|
||||
return messageMediaImageInteractiveFetched(fetchManager: context.fetchManager, messageId: message.id, messageReference: MessageReference(message), image: image, resource: resource, userInitiated: true, priority: .userInitiated, storeToDownloadsPeerType: storeToDownloadsPeerType)
|
||||
public func messageMediaImageInteractiveFetched(context: AccountContext, message: Message, image: TelegramMediaImage, resource: MediaResource, range: Range<Int>? = nil, storeToDownloadsPeerType: MediaAutoDownloadPeerType?) -> Signal<Void, NoError> {
|
||||
return messageMediaImageInteractiveFetched(fetchManager: context.fetchManager, messageId: message.id, messageReference: MessageReference(message), image: image, resource: resource, range: range, userInitiated: true, priority: .userInitiated, storeToDownloadsPeerType: storeToDownloadsPeerType)
|
||||
}
|
||||
|
||||
public func messageMediaImageInteractiveFetched(fetchManager: FetchManager, messageId: MessageId, messageReference: MessageReference, image: TelegramMediaImage, resource: MediaResource, userInitiated: Bool, priority: FetchManagerPriority, storeToDownloadsPeerType: MediaAutoDownloadPeerType?) -> Signal<Void, NoError> {
|
||||
public func messageMediaImageInteractiveFetched(fetchManager: FetchManager, messageId: MessageId, messageReference: MessageReference, image: TelegramMediaImage, resource: MediaResource, range: Range<Int>? = nil, userInitiated: Bool, priority: FetchManagerPriority, storeToDownloadsPeerType: MediaAutoDownloadPeerType?) -> Signal<Void, NoError> {
|
||||
let mediaReference = AnyMediaReference.message(message: messageReference, media: image)
|
||||
return fetchManager.interactivelyFetched(category: .image, location: .chat(messageId.peerId), locationKey: .messageId(messageId), mediaReference: mediaReference, resourceReference: mediaReference.resourceReference(resource), ranges: FetchCompleteRange, statsCategory: .image, elevatedPriority: false, userInitiated: userInitiated, priority: priority, storeToDownloadsPeerType: storeToDownloadsPeerType)
|
||||
let ranges: IndexSet
|
||||
if let range = range {
|
||||
ranges = IndexSet(integersIn: range)
|
||||
} else {
|
||||
ranges = FetchCompleteRange
|
||||
}
|
||||
return fetchManager.interactivelyFetched(category: .image, location: .chat(messageId.peerId), locationKey: .messageId(messageId), mediaReference: mediaReference, resourceReference: mediaReference.resourceReference(resource), ranges: ranges, statsCategory: .image, elevatedPriority: false, userInitiated: userInitiated, priority: priority, storeToDownloadsPeerType: storeToDownloadsPeerType)
|
||||
}
|
||||
|
||||
public func messageMediaImageCancelInteractiveFetch(context: AccountContext, messageId: MessageId, image: TelegramMediaImage, resource: MediaResource) {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import TelegramAudio
|
|||
|
||||
public enum RequestCallResult {
|
||||
case requested
|
||||
case alreadyInProgress(PeerId)
|
||||
case alreadyInProgress(PeerId?)
|
||||
}
|
||||
|
||||
public struct CallAuxiliaryServer {
|
||||
|
|
@ -40,44 +40,75 @@ public struct PresentationCallState: Equatable {
|
|||
case connecting(Data?)
|
||||
case active(Double, Int32?, Data)
|
||||
case reconnecting(Double, Int32?, Data)
|
||||
case terminating
|
||||
case terminating(CallSessionTerminationReason?)
|
||||
case terminated(CallId?, CallSessionTerminationReason?, Bool)
|
||||
}
|
||||
|
||||
public enum VideoState: Equatable {
|
||||
case notAvailable
|
||||
case possible
|
||||
case outgoingRequested
|
||||
case incomingRequested
|
||||
case inactive
|
||||
case active
|
||||
case paused
|
||||
}
|
||||
|
||||
public enum RemoteVideoState: Equatable {
|
||||
case inactive
|
||||
case active
|
||||
case paused
|
||||
}
|
||||
|
||||
public enum RemoteAudioState: Equatable {
|
||||
case active
|
||||
case muted
|
||||
}
|
||||
|
||||
public enum RemoteBatteryLevel: Equatable {
|
||||
case normal
|
||||
case low
|
||||
}
|
||||
|
||||
public var state: State
|
||||
public var videoState: VideoState
|
||||
public var remoteVideoState: RemoteVideoState
|
||||
public var remoteAudioState: RemoteAudioState
|
||||
public var remoteBatteryLevel: RemoteBatteryLevel
|
||||
|
||||
public init(state: State, videoState: VideoState, remoteVideoState: RemoteVideoState) {
|
||||
public init(state: State, videoState: VideoState, remoteVideoState: RemoteVideoState, remoteAudioState: RemoteAudioState, remoteBatteryLevel: RemoteBatteryLevel) {
|
||||
self.state = state
|
||||
self.videoState = videoState
|
||||
self.remoteVideoState = remoteVideoState
|
||||
self.remoteAudioState = remoteAudioState
|
||||
self.remoteBatteryLevel = remoteBatteryLevel
|
||||
}
|
||||
}
|
||||
|
||||
public final class PresentationCallVideoView {
|
||||
public enum Orientation {
|
||||
case rotation0
|
||||
case rotation90
|
||||
case rotation180
|
||||
case rotation270
|
||||
}
|
||||
|
||||
public let view: UIView
|
||||
public let setOnFirstFrameReceived: ((() -> Void)?) -> Void
|
||||
public let setOnFirstFrameReceived: (((Float) -> Void)?) -> Void
|
||||
|
||||
public let getOrientation: () -> Orientation
|
||||
public let setOnOrientationUpdated: (((Orientation) -> Void)?) -> Void
|
||||
public let setOnIsMirroredUpdated: (((Bool) -> Void)?) -> Void
|
||||
|
||||
public init(
|
||||
view: UIView,
|
||||
setOnFirstFrameReceived: @escaping ((() -> Void)?) -> Void
|
||||
setOnFirstFrameReceived: @escaping (((Float) -> Void)?) -> Void,
|
||||
getOrientation: @escaping () -> Orientation,
|
||||
setOnOrientationUpdated: @escaping (((Orientation) -> Void)?) -> Void,
|
||||
setOnIsMirroredUpdated: @escaping (((Bool) -> Void)?) -> Void
|
||||
) {
|
||||
self.view = view
|
||||
self.setOnFirstFrameReceived = setOnFirstFrameReceived
|
||||
self.getOrientation = getOrientation
|
||||
self.setOnOrientationUpdated = setOnOrientationUpdated
|
||||
self.setOnIsMirroredUpdated = setOnIsMirroredUpdated
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +137,7 @@ public protocol PresentationCall: class {
|
|||
func toggleIsMuted()
|
||||
func setIsMuted(_ value: Bool)
|
||||
func requestVideo()
|
||||
func acceptVideo()
|
||||
func disableVideo()
|
||||
func setOutgoingVideoIsPaused(_ isPaused: Bool)
|
||||
func switchVideoCamera()
|
||||
func setCurrentAudioOutput(_ output: AudioSessionOutput)
|
||||
|
|
@ -119,5 +150,5 @@ public protocol PresentationCall: class {
|
|||
public protocol PresentationCallManager: class {
|
||||
var currentCallSignal: Signal<PresentationCall?, NoError> { get }
|
||||
|
||||
func requestCall(account: Account, peerId: PeerId, isVideo: Bool, endCurrentIfAny: Bool) -> RequestCallResult
|
||||
func requestCall(context: AccountContext, peerId: PeerId, isVideo: Bool, endCurrentIfAny: Bool) -> RequestCallResult
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
import SyncCore
|
||||
|
||||
public struct VideoCallsConfiguration: Equatable {
|
||||
public enum VideoCallsSupport {
|
||||
case disabled
|
||||
case full
|
||||
case onlyVideo
|
||||
}
|
||||
|
||||
public var videoCallsSupport: VideoCallsSupport
|
||||
|
||||
public init(appConfiguration: AppConfiguration) {
|
||||
var videoCallsSupport: VideoCallsSupport = .full
|
||||
if let data = appConfiguration.data, let value = data["video_calls_support"] as? String {
|
||||
switch value {
|
||||
case "disabled":
|
||||
videoCallsSupport = .disabled
|
||||
case "full":
|
||||
videoCallsSupport = .full
|
||||
case "only_video":
|
||||
videoCallsSupport = .onlyVideo
|
||||
default:
|
||||
videoCallsSupport = .full
|
||||
}
|
||||
}
|
||||
self.videoCallsSupport = videoCallsSupport
|
||||
}
|
||||
}
|
||||
|
||||
public extension VideoCallsConfiguration {
|
||||
var areVideoCallsEnabled: Bool {
|
||||
switch self.videoCallsSupport {
|
||||
case .disabled:
|
||||
return false
|
||||
case .full, .onlyVideo:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,8 +5,10 @@ import Display
|
|||
import AsyncDisplayKit
|
||||
import RLottieBinding
|
||||
import GZip
|
||||
import YuvConversion
|
||||
|
||||
private let sharedQueue = Queue()
|
||||
private let sharedStoreQueue = Queue.concurrentDefaultQueue()
|
||||
|
||||
private class AnimatedStickerNodeDisplayEvents: ASDisplayNode {
|
||||
private var value: Bool = false
|
||||
|
|
@ -46,12 +48,13 @@ private class AnimatedStickerNodeDisplayEvents: ASDisplayNode {
|
|||
|
||||
public enum AnimatedStickerMode {
|
||||
case cached
|
||||
case direct
|
||||
case direct(cachePathPrefix: String?)
|
||||
}
|
||||
|
||||
public enum AnimatedStickerPlaybackPosition {
|
||||
case start
|
||||
case end
|
||||
case timestamp(Double)
|
||||
}
|
||||
|
||||
public enum AnimatedStickerPlaybackMode {
|
||||
|
|
@ -83,8 +86,9 @@ public final class AnimatedStickerFrame {
|
|||
public protocol AnimatedStickerFrameSource: class {
|
||||
var frameRate: Int { get }
|
||||
var frameCount: Int { get }
|
||||
var frameIndex: Int { get }
|
||||
|
||||
func takeFrame() -> AnimatedStickerFrame?
|
||||
func takeFrame(draw: Bool) -> AnimatedStickerFrame?
|
||||
func skipToEnd()
|
||||
}
|
||||
|
||||
|
|
@ -109,7 +113,7 @@ public final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource
|
|||
let height: Int
|
||||
public let frameRate: Int
|
||||
public let frameCount: Int
|
||||
private var frameIndex: Int
|
||||
public var frameIndex: Int
|
||||
private let initialOffset: Int
|
||||
private var offset: Int
|
||||
var decodeBuffer: Data
|
||||
|
|
@ -179,7 +183,7 @@ public final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource
|
|||
assert(self.queue.isCurrent())
|
||||
}
|
||||
|
||||
public func takeFrame() -> AnimatedStickerFrame? {
|
||||
public func takeFrame(draw: Bool) -> AnimatedStickerFrame? {
|
||||
var frameData: Data?
|
||||
var isLastFrame = false
|
||||
|
||||
|
|
@ -210,27 +214,29 @@ public final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource
|
|||
|
||||
self.offset += 4
|
||||
|
||||
self.scratchBuffer.withUnsafeMutableBytes { (scratchBytes: UnsafeMutablePointer<UInt8>) -> Void in
|
||||
self.decodeBuffer.withUnsafeMutableBytes { (decodeBytes: UnsafeMutablePointer<UInt8>) -> Void in
|
||||
self.frameBuffer.withUnsafeMutableBytes { (frameBytes: UnsafeMutablePointer<UInt8>) -> Void in
|
||||
compression_decode_buffer(decodeBytes, decodeBufferLength, bytes.advanced(by: self.offset), Int(frameLength), UnsafeMutableRawPointer(scratchBytes), COMPRESSION_LZFSE)
|
||||
|
||||
var lhs = UnsafeMutableRawPointer(frameBytes).assumingMemoryBound(to: UInt64.self)
|
||||
var rhs = UnsafeRawPointer(decodeBytes).assumingMemoryBound(to: UInt64.self)
|
||||
for _ in 0 ..< decodeBufferLength / 8 {
|
||||
lhs.pointee = lhs.pointee ^ rhs.pointee
|
||||
lhs = lhs.advanced(by: 1)
|
||||
rhs = rhs.advanced(by: 1)
|
||||
if draw {
|
||||
self.scratchBuffer.withUnsafeMutableBytes { (scratchBytes: UnsafeMutablePointer<UInt8>) -> Void in
|
||||
self.decodeBuffer.withUnsafeMutableBytes { (decodeBytes: UnsafeMutablePointer<UInt8>) -> Void in
|
||||
self.frameBuffer.withUnsafeMutableBytes { (frameBytes: UnsafeMutablePointer<UInt8>) -> Void in
|
||||
compression_decode_buffer(decodeBytes, decodeBufferLength, bytes.advanced(by: self.offset), Int(frameLength), UnsafeMutableRawPointer(scratchBytes), COMPRESSION_LZFSE)
|
||||
|
||||
var lhs = UnsafeMutableRawPointer(frameBytes).assumingMemoryBound(to: UInt64.self)
|
||||
var rhs = UnsafeRawPointer(decodeBytes).assumingMemoryBound(to: UInt64.self)
|
||||
for _ in 0 ..< decodeBufferLength / 8 {
|
||||
lhs.pointee = lhs.pointee ^ rhs.pointee
|
||||
lhs = lhs.advanced(by: 1)
|
||||
rhs = rhs.advanced(by: 1)
|
||||
}
|
||||
var lhsRest = UnsafeMutableRawPointer(frameBytes).assumingMemoryBound(to: UInt8.self).advanced(by: (decodeBufferLength / 8) * 8)
|
||||
var rhsRest = UnsafeMutableRawPointer(decodeBytes).assumingMemoryBound(to: UInt8.self).advanced(by: (decodeBufferLength / 8) * 8)
|
||||
for _ in (decodeBufferLength / 8) * 8 ..< decodeBufferLength {
|
||||
lhsRest.pointee = rhsRest.pointee ^ lhsRest.pointee
|
||||
lhsRest = lhsRest.advanced(by: 1)
|
||||
rhsRest = rhsRest.advanced(by: 1)
|
||||
}
|
||||
|
||||
frameData = Data(bytes: frameBytes, count: decodeBufferLength)
|
||||
}
|
||||
var lhsRest = UnsafeMutableRawPointer(frameBytes).assumingMemoryBound(to: UInt8.self).advanced(by: (decodeBufferLength / 8) * 8)
|
||||
var rhsRest = UnsafeMutableRawPointer(decodeBytes).assumingMemoryBound(to: UInt8.self).advanced(by: (decodeBufferLength / 8) * 8)
|
||||
for _ in (decodeBufferLength / 8) * 8 ..< decodeBufferLength {
|
||||
lhsRest.pointee = rhsRest.pointee ^ lhsRest.pointee
|
||||
lhsRest = lhsRest.advanced(by: 1)
|
||||
rhsRest = rhsRest.advanced(by: 1)
|
||||
}
|
||||
|
||||
frameData = Data(bytes: frameBytes, count: decodeBufferLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -247,7 +253,7 @@ public final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource
|
|||
}
|
||||
}
|
||||
|
||||
if let frameData = frameData {
|
||||
if let frameData = frameData, draw {
|
||||
return AnimatedStickerFrame(data: frameData, type: .yuva, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: isLastFrame)
|
||||
} else {
|
||||
return nil
|
||||
|
|
@ -263,18 +269,332 @@ public final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource
|
|||
}
|
||||
}
|
||||
|
||||
private func wrappedWrite(_ fd: Int32, _ data: UnsafeRawPointer, _ count: Int) -> Int {
|
||||
return write(fd, data, count)
|
||||
}
|
||||
|
||||
private func wrappedRead(_ fd: Int32, _ data: UnsafeMutableRawPointer, _ count: Int) -> Int {
|
||||
return read(fd, data, count)
|
||||
}
|
||||
|
||||
//TODO: separate ManagedFile into its own module
|
||||
private final class ManagedFileImpl {
|
||||
enum Mode {
|
||||
case read
|
||||
case readwrite
|
||||
case append
|
||||
}
|
||||
|
||||
private let queue: Queue?
|
||||
private let fd: Int32
|
||||
private let mode: Mode
|
||||
|
||||
init?(queue: Queue?, path: String, mode: Mode) {
|
||||
if let queue = queue {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
self.queue = queue
|
||||
self.mode = mode
|
||||
let fileMode: Int32
|
||||
let accessMode: UInt16
|
||||
switch mode {
|
||||
case .read:
|
||||
fileMode = O_RDONLY
|
||||
accessMode = S_IRUSR
|
||||
case .readwrite:
|
||||
fileMode = O_RDWR | O_CREAT
|
||||
accessMode = S_IRUSR | S_IWUSR
|
||||
case .append:
|
||||
fileMode = O_WRONLY | O_CREAT | O_APPEND
|
||||
accessMode = S_IRUSR | S_IWUSR
|
||||
}
|
||||
let fd = open(path, fileMode, accessMode)
|
||||
if fd >= 0 {
|
||||
self.fd = fd
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let queue = self.queue {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
close(self.fd)
|
||||
}
|
||||
|
||||
public func write(_ data: UnsafeRawPointer, count: Int) -> Int {
|
||||
if let queue = self.queue {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
return wrappedWrite(self.fd, data, count)
|
||||
}
|
||||
|
||||
public func read(_ data: UnsafeMutableRawPointer, _ count: Int) -> Int {
|
||||
if let queue = self.queue {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
return wrappedRead(self.fd, data, count)
|
||||
}
|
||||
|
||||
public func readData(count: Int) -> Data {
|
||||
if let queue = self.queue {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
var result = Data(count: count)
|
||||
result.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<Int8>) -> Void in
|
||||
let readCount = self.read(bytes, count)
|
||||
assert(readCount == count)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func seek(position: Int64) {
|
||||
if let queue = self.queue {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
lseek(self.fd, position, SEEK_SET)
|
||||
}
|
||||
|
||||
public func truncate(count: Int64) {
|
||||
if let queue = self.queue {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
ftruncate(self.fd, count)
|
||||
}
|
||||
|
||||
public func getSize() -> Int? {
|
||||
if let queue = self.queue {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
var value = stat()
|
||||
if fstat(self.fd, &value) == 0 {
|
||||
return Int(value.st_size)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func sync() {
|
||||
if let queue = self.queue {
|
||||
assert(queue.isCurrent())
|
||||
}
|
||||
fsync(self.fd)
|
||||
}
|
||||
}
|
||||
|
||||
private func compressFrame(width: Int, height: Int, rgbData: Data) -> Data? {
|
||||
let bytesPerRow = rgbData.count / height
|
||||
|
||||
let yuvaPixelsPerAlphaRow = (Int(width) + 1) & (~1)
|
||||
assert(yuvaPixelsPerAlphaRow % 2 == 0)
|
||||
|
||||
let yuvaLength = Int(width) * Int(height) * 2 + yuvaPixelsPerAlphaRow * Int(height) / 2
|
||||
var yuvaFrameData = malloc(yuvaLength)!
|
||||
defer {
|
||||
free(yuvaFrameData)
|
||||
}
|
||||
memset(yuvaFrameData, 0, yuvaLength)
|
||||
|
||||
var compressedFrameData = Data(count: yuvaLength)
|
||||
let compressedFrameDataLength = compressedFrameData.count
|
||||
|
||||
let scratchData = malloc(compression_encode_scratch_buffer_size(COMPRESSION_LZFSE))!
|
||||
defer {
|
||||
free(scratchData)
|
||||
}
|
||||
|
||||
var rgbData = rgbData
|
||||
rgbData.withUnsafeMutableBytes { (buffer: UnsafeMutableRawBufferPointer) -> Void in
|
||||
if let baseAddress = buffer.baseAddress {
|
||||
encodeRGBAToYUVA(yuvaFrameData.assumingMemoryBound(to: UInt8.self), baseAddress.assumingMemoryBound(to: UInt8.self), Int32(width), Int32(height), Int32(bytesPerRow))
|
||||
}
|
||||
}
|
||||
|
||||
var maybeResultSize: Int?
|
||||
|
||||
compressedFrameData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) -> Void in
|
||||
let length = compression_encode_buffer(bytes, compressedFrameDataLength, yuvaFrameData.assumingMemoryBound(to: UInt8.self), yuvaLength, scratchData, COMPRESSION_LZFSE)
|
||||
maybeResultSize = length
|
||||
}
|
||||
|
||||
guard let resultSize = maybeResultSize else {
|
||||
return nil
|
||||
}
|
||||
compressedFrameData.count = resultSize
|
||||
return compressedFrameData
|
||||
}
|
||||
|
||||
private final class AnimatedStickerDirectFrameSourceCache {
|
||||
private let queue: Queue
|
||||
private let storeQueue: Queue
|
||||
private let file: ManagedFileImpl
|
||||
private let frameCount: Int
|
||||
private let width: Int
|
||||
private let height: Int
|
||||
|
||||
private var isStoringFrames = Set<Int>()
|
||||
|
||||
private var scratchBuffer: Data
|
||||
private var decodeBuffer: Data
|
||||
|
||||
init?(queue: Queue, pathPrefix: String, width: Int, height: Int, frameCount: Int) {
|
||||
self.queue = queue
|
||||
self.storeQueue = sharedStoreQueue
|
||||
|
||||
self.frameCount = frameCount
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
let path = "\(pathPrefix)_\(width):\(height).stickerframecache"
|
||||
var file = ManagedFileImpl(queue: queue, path: path, mode: .readwrite)
|
||||
if let file = file {
|
||||
self.file = file
|
||||
} else {
|
||||
let _ = try? FileManager.default.removeItem(atPath: path)
|
||||
file = ManagedFileImpl(queue: queue, path: path, mode: .readwrite)
|
||||
if let file = file {
|
||||
self.file = file
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
self.scratchBuffer = Data(count: compression_decode_scratch_buffer_size(COMPRESSION_LZFSE))
|
||||
|
||||
let yuvaPixelsPerAlphaRow = (Int(width) + 1) & (~1)
|
||||
let yuvaLength = Int(width) * Int(height) * 2 + yuvaPixelsPerAlphaRow * Int(height) / 2
|
||||
self.decodeBuffer = Data(count: yuvaLength)
|
||||
|
||||
self.initializeFrameTable()
|
||||
}
|
||||
|
||||
private func initializeFrameTable() {
|
||||
if let size = self.file.getSize(), size >= self.frameCount * 4 * 2 {
|
||||
} else {
|
||||
self.file.truncate(count: 0)
|
||||
for _ in 0 ..< self.frameCount {
|
||||
var zero: Int32 = 0
|
||||
let _ = self.file.write(&zero, count: 4)
|
||||
let _ = self.file.write(&zero, count: 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func readFrameRange(index: Int) -> Range<Int>? {
|
||||
if index < 0 || index >= self.frameCount {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.file.seek(position: Int64(index * 4 * 2))
|
||||
var offset: Int32 = 0
|
||||
var length: Int32 = 0
|
||||
if self.file.read(&offset, 4) != 4 {
|
||||
return nil
|
||||
}
|
||||
if self.file.read(&length, 4) != 4 {
|
||||
return nil
|
||||
}
|
||||
if length == 0 {
|
||||
return nil
|
||||
}
|
||||
if length < 0 || offset < 0 {
|
||||
return nil
|
||||
}
|
||||
return (Int(offset) ..< Int(offset + length))
|
||||
}
|
||||
|
||||
func storeUncompressedRgbFrame(index: Int, rgbData: Data) {
|
||||
if index < 0 || index >= self.frameCount {
|
||||
return
|
||||
}
|
||||
if self.isStoringFrames.contains(index) {
|
||||
return
|
||||
}
|
||||
self.isStoringFrames.insert(index)
|
||||
|
||||
let width = self.width
|
||||
let height = self.height
|
||||
|
||||
let queue = self.queue
|
||||
self.storeQueue.async { [weak self] in
|
||||
let compressedData = compressFrame(width: width, height: height, rgbData: rgbData)
|
||||
|
||||
queue.async {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
guard let currentSize = strongSelf.file.getSize() else {
|
||||
return
|
||||
}
|
||||
guard let compressedData = compressedData else {
|
||||
return
|
||||
}
|
||||
|
||||
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))
|
||||
compressedData.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) -> Void in
|
||||
if let baseAddress = buffer.baseAddress {
|
||||
let _ = strongSelf.file.write(baseAddress, count: Int(length))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readUncompressedYuvFrame(index: Int) -> Data? {
|
||||
if index < 0 || index >= self.frameCount {
|
||||
return nil
|
||||
}
|
||||
guard let range = self.readFrameRange(index: index) else {
|
||||
return nil
|
||||
}
|
||||
self.file.seek(position: Int64(range.lowerBound))
|
||||
let length = range.upperBound - range.lowerBound
|
||||
let compressedData = self.file.readData(count: length)
|
||||
if compressedData.count != length {
|
||||
return nil
|
||||
}
|
||||
|
||||
var frameData: Data?
|
||||
|
||||
let decodeBufferLength = self.decodeBuffer.count
|
||||
|
||||
compressedData.withUnsafeBytes { (bytes: UnsafePointer<UInt8>) -> Void in
|
||||
self.scratchBuffer.withUnsafeMutableBytes { (scratchBytes: UnsafeMutablePointer<UInt8>) -> Void in
|
||||
self.decodeBuffer.withUnsafeMutableBytes { (decodeBytes: UnsafeMutablePointer<UInt8>) -> Void in
|
||||
let resultLength = compression_decode_buffer(decodeBytes, decodeBufferLength, bytes, length, UnsafeMutableRawPointer(scratchBytes), COMPRESSION_LZFSE)
|
||||
|
||||
frameData = Data(bytes: decodeBytes, count: resultLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return frameData
|
||||
}
|
||||
}
|
||||
|
||||
private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource {
|
||||
private let queue: Queue
|
||||
private let data: Data
|
||||
private let width: Int
|
||||
private let height: Int
|
||||
private let cache: AnimatedStickerDirectFrameSourceCache?
|
||||
private let bytesPerRow: Int
|
||||
let frameCount: Int
|
||||
let frameRate: Int
|
||||
private var currentFrame: Int
|
||||
fileprivate var currentFrame: Int
|
||||
private let animation: LottieInstance
|
||||
|
||||
init?(queue: Queue, data: Data, width: Int, height: Int) {
|
||||
var frameIndex: Int {
|
||||
return self.currentFrame % self.frameCount
|
||||
}
|
||||
|
||||
init?(queue: Queue, data: Data, width: Int, height: Int, cachePathPrefix: String?) {
|
||||
self.queue = queue
|
||||
self.data = data
|
||||
self.width = width
|
||||
|
|
@ -286,23 +606,39 @@ private final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource
|
|||
return nil
|
||||
}
|
||||
self.animation = animation
|
||||
self.frameCount = Int(animation.frameCount)
|
||||
let frameCount = Int(animation.frameCount)
|
||||
self.frameCount = frameCount
|
||||
self.frameRate = Int(animation.frameRate)
|
||||
|
||||
self.cache = cachePathPrefix.flatMap { cachePathPrefix in
|
||||
AnimatedStickerDirectFrameSourceCache(queue: queue, pathPrefix: cachePathPrefix, width: width, height: height, frameCount: frameCount)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
assert(self.queue.isCurrent())
|
||||
}
|
||||
|
||||
func takeFrame() -> AnimatedStickerFrame? {
|
||||
func takeFrame(draw: Bool) -> AnimatedStickerFrame? {
|
||||
let frameIndex = self.currentFrame % self.frameCount
|
||||
self.currentFrame += 1
|
||||
var frameData = Data(count: self.bytesPerRow * self.height)
|
||||
frameData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) -> Void in
|
||||
memset(bytes, 0, self.bytesPerRow * self.height)
|
||||
self.animation.renderFrame(with: Int32(frameIndex), into: bytes, width: Int32(self.width), height: Int32(self.height), bytesPerRow: Int32(self.bytesPerRow))
|
||||
if draw {
|
||||
if let cache = self.cache, let yuvData = cache.readUncompressedYuvFrame(index: frameIndex) {
|
||||
return AnimatedStickerFrame(data: yuvData, type: .yuva, width: self.width, height: self.height, bytesPerRow: 0, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1)
|
||||
} else {
|
||||
var frameData = Data(count: self.bytesPerRow * self.height)
|
||||
frameData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) -> Void in
|
||||
memset(bytes, 0, self.bytesPerRow * self.height)
|
||||
self.animation.renderFrame(with: Int32(frameIndex), into: bytes, width: Int32(self.width), height: Int32(self.height), bytesPerRow: Int32(self.bytesPerRow))
|
||||
}
|
||||
if let cache = self.cache {
|
||||
cache.storeUncompressedRgbFrame(index: frameIndex, rgbData: frameData)
|
||||
}
|
||||
return AnimatedStickerFrame(data: frameData, type: .argb, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1)
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
return AnimatedStickerFrame(data: frameData, type: .argb, width: self.width, height: self.height, bytesPerRow: self.bytesPerRow, index: frameIndex, isLastFrame: frameIndex == self.frameCount - 1)
|
||||
}
|
||||
|
||||
func skipToEnd() {
|
||||
|
|
@ -326,9 +662,9 @@ public final class AnimatedStickerFrameQueue {
|
|||
assert(self.queue.isCurrent())
|
||||
}
|
||||
|
||||
public func take() -> AnimatedStickerFrame? {
|
||||
public func take(draw: Bool) -> AnimatedStickerFrame? {
|
||||
if self.frames.isEmpty {
|
||||
if let frame = self.source.takeFrame() {
|
||||
if let frame = self.source.takeFrame(draw: draw) {
|
||||
self.frames.append(frame)
|
||||
}
|
||||
}
|
||||
|
|
@ -342,7 +678,7 @@ public final class AnimatedStickerFrameQueue {
|
|||
|
||||
public func generateFramesIfNeeded() {
|
||||
if self.frames.isEmpty {
|
||||
if let frame = self.source.takeFrame() {
|
||||
if let frame = self.source.takeFrame(draw: true) {
|
||||
self.frames.append(frame)
|
||||
}
|
||||
}
|
||||
|
|
@ -397,7 +733,7 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||
private let timer = Atomic<SwiftSignalKit.Timer?>(value: nil)
|
||||
private let frameSource = Atomic<QueueLocalObject<AnimatedStickerFrameSourceWrapper>?>(value: nil)
|
||||
|
||||
private var directData: (Data, String, Int, Int)?
|
||||
private var directData: (Data, String, Int, Int, String?)?
|
||||
private var cachedData: (Data, Bool)?
|
||||
|
||||
private var renderer: (AnimationRenderer & ASDisplayNode)?
|
||||
|
|
@ -467,13 +803,13 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||
}
|
||||
self.playbackMode = playbackMode
|
||||
switch mode {
|
||||
case .direct:
|
||||
case let .direct(cachePathPrefix):
|
||||
let f: (String) -> Void = { [weak self] path in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
if let directData = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) {
|
||||
strongSelf.directData = (directData, path, width, height)
|
||||
strongSelf.directData = (directData, path, width, height, cachePathPrefix)
|
||||
}
|
||||
if case let .still(position) = playbackMode {
|
||||
strongSelf.seekTo(position)
|
||||
|
|
@ -556,7 +892,7 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||
if maybeFrameSource == nil {
|
||||
let notifyUpdated: (() -> Void)? = nil
|
||||
if let directData = directData {
|
||||
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3)
|
||||
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3, cachePathPrefix: directData.4)
|
||||
} else if let (cachedData, cachedDataComplete) = cachedData {
|
||||
if #available(iOS 9.0, *) {
|
||||
maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: {
|
||||
|
|
@ -583,7 +919,7 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||
|
||||
let timer = SwiftSignalKit.Timer(timeout: 1.0 / Double(frameRate), repeat: !firstFrame, completion: {
|
||||
let maybeFrame = frameQueue.syncWith { frameQueue in
|
||||
return frameQueue.take()
|
||||
return frameQueue.take(draw: true)
|
||||
}
|
||||
if let maybeFrame = maybeFrame, let frame = maybeFrame {
|
||||
Queue.mainQueue().async {
|
||||
|
|
@ -628,7 +964,7 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||
var maybeFrameSource: AnimatedStickerFrameSource?
|
||||
let notifyUpdated: (() -> Void)? = nil
|
||||
if let directData = directData {
|
||||
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3)
|
||||
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3, cachePathPrefix: directData.4)
|
||||
} else if let (cachedData, cachedDataComplete) = cachedData {
|
||||
if #available(iOS 9.0, *) {
|
||||
maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: {
|
||||
|
|
@ -654,7 +990,7 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||
|
||||
let timer = SwiftSignalKit.Timer(timeout: 1.0 / Double(frameRate), repeat: !firstFrame, completion: {
|
||||
let maybeFrame = frameQueue.syncWith { frameQueue in
|
||||
return frameQueue.take()
|
||||
return frameQueue.take(draw: true)
|
||||
}
|
||||
if let maybeFrame = maybeFrame, let frame = maybeFrame {
|
||||
Queue.mainQueue().async {
|
||||
|
|
@ -710,19 +1046,25 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||
let directData = self.directData
|
||||
let cachedData = self.cachedData
|
||||
let queue = self.queue
|
||||
let frameSourceHolder = self.frameSource
|
||||
let timerHolder = self.timer
|
||||
self.queue.async { [weak self] in
|
||||
var maybeFrameSource: AnimatedStickerFrameSource?
|
||||
if let directData = directData {
|
||||
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3)
|
||||
if position == .end {
|
||||
maybeFrameSource?.skipToEnd()
|
||||
}
|
||||
} else if let (cachedData, cachedDataComplete) = cachedData {
|
||||
if #available(iOS 9.0, *) {
|
||||
maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: {})
|
||||
var maybeFrameSource: AnimatedStickerFrameSource? = frameSourceHolder.with { $0 }?.syncWith { $0 }?.value
|
||||
if case .timestamp = position {
|
||||
} else {
|
||||
var maybeFrameSource: AnimatedStickerFrameSource?
|
||||
if let directData = directData {
|
||||
maybeFrameSource = AnimatedStickerDirectFrameSource(queue: queue, data: directData.0, width: directData.2, height: directData.3, cachePathPrefix: directData.4)
|
||||
if case .end = position {
|
||||
maybeFrameSource?.skipToEnd()
|
||||
}
|
||||
} else if let (cachedData, cachedDataComplete) = cachedData {
|
||||
if #available(iOS 9.0, *) {
|
||||
maybeFrameSource = AnimatedStickerCachedFrameSource(queue: queue, data: cachedData, complete: cachedDataComplete, notifyUpdated: {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
guard let frameSource = maybeFrameSource else {
|
||||
return
|
||||
}
|
||||
|
|
@ -732,9 +1074,31 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||
timerHolder.swap(nil)?.invalidate()
|
||||
|
||||
let duration: Double = frameSource.frameRate > 0 ? Double(frameSource.frameCount) / Double(frameSource.frameRate) : 0
|
||||
|
||||
let maybeFrame = frameQueue.syncWith { frameQueue in
|
||||
return frameQueue.take()
|
||||
|
||||
var maybeFrame: AnimatedStickerFrame??
|
||||
if case let .timestamp(timestamp) = position {
|
||||
var stickerTimestamp = timestamp
|
||||
while stickerTimestamp > duration {
|
||||
stickerTimestamp -= duration
|
||||
}
|
||||
let targetFrame = Int(stickerTimestamp / duration * Double(frameSource.frameCount))
|
||||
if targetFrame == frameSource.frameIndex {
|
||||
return
|
||||
}
|
||||
|
||||
var delta = targetFrame - frameSource.frameIndex
|
||||
if delta < 0 {
|
||||
delta = frameSource.frameCount + delta
|
||||
}
|
||||
for i in 0 ..< delta {
|
||||
maybeFrame = frameQueue.syncWith { frameQueue in
|
||||
return frameQueue.take(draw: i == delta - 1)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
maybeFrame = frameQueue.syncWith { frameQueue in
|
||||
return frameQueue.take(draw: true)
|
||||
}
|
||||
}
|
||||
if let maybeFrame = maybeFrame, let frame = maybeFrame {
|
||||
Queue.mainQueue().async {
|
||||
|
|
|
|||
|
|
@ -8,8 +8,13 @@ import YuvConversion
|
|||
final class SoftwareAnimationRenderer: ASDisplayNode, AnimationRenderer {
|
||||
func render(queue: Queue, width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, completion: @escaping () -> Void) {
|
||||
queue.async { [weak self] in
|
||||
let calculatedBytesPerRow = (4 * Int(width) + 15) & (~15)
|
||||
assert(bytesPerRow == calculatedBytesPerRow)
|
||||
switch type {
|
||||
case .argb:
|
||||
let calculatedBytesPerRow = (4 * Int(width) + 15) & (~15)
|
||||
assert(bytesPerRow == calculatedBytesPerRow)
|
||||
case .yuva:
|
||||
break
|
||||
}
|
||||
|
||||
let image = generateImagePixel(CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, pixelGenerator: { _, pixelData, bytesPerRow in
|
||||
switch type {
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ public final class AuthDataTransferSplashScreen: ViewController {
|
|||
return
|
||||
}
|
||||
|
||||
DeviceAccess.authorizeAccess(to: .camera, presentationData: strongSelf.presentationData, present: { c, a in
|
||||
DeviceAccess.authorizeAccess(to: .camera(.video), presentationData: strongSelf.presentationData, present: { c, a in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -368,6 +368,7 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
|
|||
var hasMissed = false
|
||||
var hasIncoming = false
|
||||
var hasOutgoing = false
|
||||
var isVideo = false
|
||||
|
||||
var hadDuration = false
|
||||
var callDuration: Int32?
|
||||
|
|
@ -375,7 +376,8 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
|
|||
for message in item.messages {
|
||||
inner: for media in message.media {
|
||||
if let action = media as? TelegramMediaAction {
|
||||
if case let .phoneCall(_, discardReason, duration, _) = action.action {
|
||||
if case let .phoneCall(_, discardReason, duration, video) = action.action {
|
||||
isVideo = video
|
||||
if message.flags.contains(.Incoming) {
|
||||
hasIncoming = true
|
||||
|
||||
|
|
@ -459,9 +461,12 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
|
|||
|
||||
let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: titleLayout.size.height + titleSpacing + statusLayout.size.height + verticalInset * 2.0), insets: UIEdgeInsets(top: firstWithHeader ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0))
|
||||
|
||||
let outgoingIcon = PresentationResourcesCallList.outgoingIcon(item.presentationData.theme)
|
||||
let outgoingVoiceIcon = PresentationResourcesCallList.outgoingIcon(item.presentationData.theme)
|
||||
let outgoingVideoIcon = PresentationResourcesCallList.outgoingVideoIcon(item.presentationData.theme)
|
||||
let infoIcon = PresentationResourcesCallList.infoButton(item.presentationData.theme)
|
||||
|
||||
let outgoingIcon = isVideo ? outgoingVideoIcon : outgoingVoiceIcon
|
||||
|
||||
let contentSize = nodeLayout.contentSize
|
||||
|
||||
return (nodeLayout, { [weak self] synchronousLoads in
|
||||
|
|
@ -582,7 +587,7 @@ class CallListCallItemNode: ItemListRevealOptionsItemNode {
|
|||
if strongSelf.typeIconNode.image !== outgoingIcon {
|
||||
strongSelf.typeIconNode.image = outgoingIcon
|
||||
}
|
||||
transition.updateFrameAdditive(node: strongSelf.typeIconNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 81.0, y: floor((nodeLayout.contentSize.height - outgoingIcon.size.height) / 2.0)), size: outgoingIcon.size))
|
||||
transition.updateFrameAdditive(node: strongSelf.typeIconNode, frame: CGRect(origin: CGPoint(x: revealOffset + leftInset - 79.0, y: floor((nodeLayout.contentSize.height - outgoingIcon.size.height) / 2.0)), size: outgoingIcon.size))
|
||||
}
|
||||
strongSelf.typeIconNode.isHidden = !hasOutgoing
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import ItemListUI
|
|||
import PresentationDataUtils
|
||||
import AccountContext
|
||||
import AlertUI
|
||||
import PresentationDataUtils
|
||||
import AppBundle
|
||||
import LocalizedPeerData
|
||||
|
||||
|
|
@ -201,18 +200,18 @@ public final class CallListController: ViewController {
|
|||
}
|
||||
|
||||
@objc func callPressed() {
|
||||
self.beginCallImpl(isVideo: false)
|
||||
self.beginCallImpl()
|
||||
}
|
||||
|
||||
private func beginCallImpl(isVideo: Bool) {
|
||||
let controller = self.context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams(context: self.context, title: { $0.Calls_NewCall }))
|
||||
private func beginCallImpl() {
|
||||
let controller = self.context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams(context: self.context, title: { $0.Calls_NewCall }, displayCallIcons: true))
|
||||
controller.navigationPresentation = .modal
|
||||
self.createActionDisposable.set((controller.result
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak controller, weak self] peer in
|
||||
controller?.dismissSearch()
|
||||
if let strongSelf = self, let contactPeer = peer, case let .peer(peer, _, _) = contactPeer {
|
||||
strongSelf.call(peer.id, isVideo: isVideo, began: {
|
||||
if let strongSelf = self, let (contactPeer, action) = peer, case let .peer(peer, _, _) = contactPeer {
|
||||
strongSelf.call(peer.id, isVideo: action == .videoCall, began: {
|
||||
if let strongSelf = self {
|
||||
let _ = (strongSelf.context.sharedContext.hasOngoingCall.get()
|
||||
|> filter { $0 }
|
||||
|
|
@ -277,7 +276,7 @@ public final class CallListController: ViewController {
|
|||
return
|
||||
}
|
||||
|
||||
let callResult = strongSelf.context.sharedContext.callManager?.requestCall(account: strongSelf.context.account, peerId: peerId, isVideo: isVideo, endCurrentIfAny: false)
|
||||
let callResult = strongSelf.context.sharedContext.callManager?.requestCall(context: strongSelf.context, peerId: peerId, isVideo: isVideo, endCurrentIfAny: false)
|
||||
if let callResult = callResult {
|
||||
if case let .alreadyInProgress(currentPeerId) = callResult {
|
||||
if currentPeerId == peerId {
|
||||
|
|
@ -286,17 +285,22 @@ public final class CallListController: ViewController {
|
|||
} else {
|
||||
let presentationData = strongSelf.presentationData
|
||||
let _ = (strongSelf.context.account.postbox.transaction { transaction -> (Peer?, Peer?) in
|
||||
return (transaction.getPeer(peerId), transaction.getPeer(currentPeerId))
|
||||
} |> deliverOnMainQueue).start(next: { [weak self] peer, current in
|
||||
if let strongSelf = self, let peer = peer, let current = current {
|
||||
return (transaction.getPeer(peerId), currentPeerId.flatMap(transaction.getPeer))
|
||||
} |> deliverOnMainQueue).start(next: { [weak self] peer, current in
|
||||
if let strongSelf = self, let peer = peer {
|
||||
if let current = current {
|
||||
strongSelf.present(textAlertController(context: strongSelf.context, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
|
||||
if let strongSelf = self {
|
||||
let _ = strongSelf.context.sharedContext.callManager?.requestCall(account: strongSelf.context.account, peerId: peerId, isVideo: isVideo, endCurrentIfAny: true)
|
||||
let _ = strongSelf.context.sharedContext.callManager?.requestCall(context: strongSelf.context, peerId: peerId, isVideo: isVideo, endCurrentIfAny: true)
|
||||
began?()
|
||||
}
|
||||
})]), in: .window(.root))
|
||||
} else {
|
||||
strongSelf.present(textAlertController(context: strongSelf.context, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_ExternalCallInProgressMessage, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
|
||||
})]), in: .window(.root))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
began?()
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ enum ChatContextMenuSource {
|
|||
case search(ChatListSearchContextActionSource)
|
||||
}
|
||||
|
||||
func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: ChatListNodeEntryPromoInfo?, source: ChatContextMenuSource, chatListController: ChatListControllerImpl?) -> Signal<[ContextMenuItem], NoError> {
|
||||
func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: ChatListNodeEntryPromoInfo?, source: ChatContextMenuSource, chatListController: ChatListControllerImpl?, joined: Bool) -> Signal<[ContextMenuItem], NoError> {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with({ $0 })
|
||||
let strings = presentationData.strings
|
||||
return context.account.postbox.transaction { [weak chatListController] transaction -> [ContextMenuItem] in
|
||||
|
|
@ -233,7 +233,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
|
|||
updatedItems.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Back, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { c, _ in
|
||||
c.setItems(chatContextMenuItems(context: context, peerId: peerId, promoInfo: promoInfo, source: source, chatListController: chatListController))
|
||||
c.setItems(chatContextMenuItems(context: context, peerId: peerId, promoInfo: promoInfo, source: source, chatListController: chatListController, joined: joined))
|
||||
})))
|
||||
|
||||
return updatedItems
|
||||
|
|
@ -379,7 +379,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
|
|||
if case .chatList = source, groupAndIndex != nil {
|
||||
items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Delete, textColor: .destructive, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { _, f in
|
||||
if let chatListController = chatListController {
|
||||
chatListController.deletePeerChat(peerId: peerId)
|
||||
chatListController.deletePeerChat(peerId: peerId, joined: joined)
|
||||
}
|
||||
f(.default)
|
||||
})))
|
||||
|
|
|
|||
|
|
@ -575,11 +575,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
|
|||
strongSelf.hidePsa(peerId)
|
||||
}
|
||||
|
||||
self.chatListDisplayNode.containerNode.deletePeerChat = { [weak self] peerId in
|
||||
self.chatListDisplayNode.containerNode.deletePeerChat = { [weak self] peerId, joined in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.deletePeerChat(peerId: peerId)
|
||||
strongSelf.deletePeerChat(peerId: peerId, joined: joined)
|
||||
}
|
||||
|
||||
self.chatListDisplayNode.containerNode.peerSelected = { [weak self] peer, animated, promoInfo in
|
||||
|
|
@ -801,6 +801,16 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
|
|||
gesture?.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
var joined = false
|
||||
if case let .peer(messages, _, _, _, _, _, _, _, _, _, _, _) = item.content, let message = messages.first {
|
||||
for media in message.media {
|
||||
if let action = media as? TelegramMediaAction, action.action == .peerJoined {
|
||||
joined = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch item.content {
|
||||
case let .groupReference(groupReference):
|
||||
let chatListController = ChatListControllerImpl(context: strongSelf.context, groupId: groupReference.groupId, controlsHistoryPreload: false, hideNetworkActivityStatus: true, previewing: true, enableDebugActions: false)
|
||||
|
|
@ -810,7 +820,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
|
|||
case let .peer(_, peer, _, _, _, _, _, _, promoInfo, _, _, _):
|
||||
let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(peer.peerId), subject: nil, botStart: nil, mode: .standard(previewing: true))
|
||||
chatController.canReadHistory.set(false)
|
||||
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peerId, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter), chatListController: strongSelf), reactionItems: [], gesture: gesture)
|
||||
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.peerId, promoInfo: promoInfo, source: .chatList(filter: strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter), chatListController: strongSelf, joined: joined), reactionItems: [], gesture: gesture)
|
||||
strongSelf.presentInGlobalOverlay(contextController)
|
||||
}
|
||||
}
|
||||
|
|
@ -823,7 +833,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
|
|||
|
||||
let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(peer.id), subject: nil, botStart: nil, mode: .standard(previewing: true))
|
||||
chatController.canReadHistory.set(false)
|
||||
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: nil, source: .search(source), chatListController: strongSelf), reactionItems: [], gesture: gesture)
|
||||
let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)), items: chatContextMenuItems(context: strongSelf.context, peerId: peer.id, promoInfo: nil, source: .search(source), chatListController: strongSelf, joined: false), reactionItems: [], gesture: gesture)
|
||||
strongSelf.presentInGlobalOverlay(contextController)
|
||||
}
|
||||
|
||||
|
|
@ -2069,7 +2079,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
|
|||
let _ = hideAccountPromoInfoChat(account: self.context.account, peerId: id).start()
|
||||
}
|
||||
|
||||
func deletePeerChat(peerId: PeerId) {
|
||||
func deletePeerChat(peerId: PeerId, joined: Bool) {
|
||||
let _ = (self.context.account.postbox.transaction { transaction -> RenderedPeer? in
|
||||
guard let peer = transaction.getPeer(peerId) else {
|
||||
return nil
|
||||
|
|
@ -2099,7 +2109,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
|
|||
}
|
||||
|
||||
if let user = chatPeer as? TelegramUser, user.botInfo == nil, canRemoveGlobally {
|
||||
strongSelf.maybeAskForPeerChatRemoval(peer: peer, completion: { _ in }, removed: {})
|
||||
strongSelf.maybeAskForPeerChatRemoval(peer: peer, joined: joined, completion: { _ in }, removed: {})
|
||||
} else {
|
||||
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
|
||||
var items: [ActionSheetItem] = []
|
||||
|
|
@ -2189,16 +2199,24 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
|
|||
if canRemoveGlobally {
|
||||
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
|
||||
var items: [ActionSheetItem] = []
|
||||
|
||||
|
||||
items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: mainPeer, chatPeer: chatPeer, action: .clearHistory, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder))
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ChatList_DeleteForEveryone(mainPeer.compactDisplayTitle).0, color: .destructive, action: { [weak actionSheet] in
|
||||
beginClear(.forEveryone)
|
||||
actionSheet?.dismissAnimated()
|
||||
}))
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak actionSheet] in
|
||||
beginClear(.forLocalPeer)
|
||||
actionSheet?.dismissAnimated()
|
||||
}))
|
||||
|
||||
if joined || mainPeer.isDeleted {
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Delete, color: .destructive, action: { [weak actionSheet] in
|
||||
beginClear(.forEveryone)
|
||||
actionSheet?.dismissAnimated()
|
||||
}))
|
||||
} else {
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ChatList_DeleteForEveryone(mainPeer.compactDisplayTitle).0, color: .destructive, action: { [weak actionSheet] in
|
||||
beginClear(.forEveryone)
|
||||
actionSheet?.dismissAnimated()
|
||||
}))
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak actionSheet] in
|
||||
beginClear(.forLocalPeer)
|
||||
actionSheet?.dismissAnimated()
|
||||
}))
|
||||
}
|
||||
|
||||
actionSheet.setItemGroups([
|
||||
ActionSheetItemGroup(items: items),
|
||||
|
|
@ -2258,7 +2276,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
|
|||
})
|
||||
}
|
||||
|
||||
public func maybeAskForPeerChatRemoval(peer: RenderedPeer, deleteGloballyIfPossible: Bool = false, completion: @escaping (Bool) -> Void, removed: @escaping () -> Void) {
|
||||
public func maybeAskForPeerChatRemoval(peer: RenderedPeer, joined: Bool = false, deleteGloballyIfPossible: Bool = false, completion: @escaping (Bool) -> Void, removed: @escaping () -> Void) {
|
||||
guard let chatPeer = peer.peers[peer.peerId], let mainPeer = peer.chatMainPeer else {
|
||||
completion(false)
|
||||
return
|
||||
|
|
@ -2279,31 +2297,41 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController,
|
|||
var items: [ActionSheetItem] = []
|
||||
|
||||
items.append(DeleteChatPeerActionSheetItem(context: self.context, peer: mainPeer, chatPeer: chatPeer, action: .delete, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder))
|
||||
items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForEveryone(mainPeer.compactDisplayTitle).0, color: .destructive, action: { [weak self, weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationText, actions: [
|
||||
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
|
||||
completion(false)
|
||||
}),
|
||||
TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationAction, action: {
|
||||
self?.schedulePeerChatRemoval(peer: peer, type: .forEveryone, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: {
|
||||
removed()
|
||||
})
|
||||
completion(true)
|
||||
})
|
||||
], parseMarkdown: true), in: .window(.root))
|
||||
}))
|
||||
items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak self, weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
self?.schedulePeerChatRemoval(peer: peer, type: .forLocalPeer, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: {
|
||||
removed()
|
||||
})
|
||||
completion(true)
|
||||
}))
|
||||
|
||||
if joined || mainPeer.isDeleted {
|
||||
items.append(ActionSheetButtonItem(title: self.presentationData.strings.Common_Delete, color: .destructive, action: { [weak self, weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
self?.schedulePeerChatRemoval(peer: peer, type: .forEveryone, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: {
|
||||
removed()
|
||||
})
|
||||
completion(true)
|
||||
}))
|
||||
} else {
|
||||
items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForEveryone(mainPeer.compactDisplayTitle).0, color: .destructive, action: { [weak self, weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationTitle, text: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationText, actions: [
|
||||
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
|
||||
completion(false)
|
||||
}),
|
||||
TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.ChatList_DeleteForEveryoneConfirmationAction, action: {
|
||||
self?.schedulePeerChatRemoval(peer: peer, type: .forEveryone, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: {
|
||||
removed()
|
||||
})
|
||||
completion(true)
|
||||
})
|
||||
], parseMarkdown: true), in: .window(.root))
|
||||
}))
|
||||
items.append(ActionSheetButtonItem(title: self.presentationData.strings.ChatList_DeleteForCurrentUser, color: .destructive, action: { [weak self, weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
self?.schedulePeerChatRemoval(peer: peer, type: .forLocalPeer, deleteGloballyIfPossible: deleteGloballyIfPossible, completion: {
|
||||
removed()
|
||||
})
|
||||
completion(true)
|
||||
}))
|
||||
}
|
||||
actionSheet.setItemGroups([
|
||||
ActionSheetItemGroup(items: items),
|
||||
ActionSheetItemGroup(items: [
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ private final class ChatListShimmerNode: ASDisplayNode {
|
|||
let timestamp1: Int32 = 100000
|
||||
let peers = SimpleDictionary<PeerId, Peer>()
|
||||
let interaction = ChatListNodeInteraction(activateSearch: {}, peerSelected: { _, _ in }, disabledPeerSelected: { _ in }, togglePeerSelected: { _ in }, additionalCategorySelected: { _ in
|
||||
}, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture in
|
||||
}, messageSelected: { _, _, _ in}, groupSelected: { _ in }, addContact: { _ in }, setPeerIdWithRevealedOptions: { _, _ in }, setItemPinned: { _, _ in }, setPeerMuted: { _, _ in }, deletePeer: { _, _ in }, updatePeerGrouping: { _, _ in }, togglePeerMarkedUnread: { _, _ in}, toggleArchivedFolderHiddenByDefault: {}, hidePsa: { _ in }, activateChatPreview: { _, _, gesture in
|
||||
gesture?.cancel()
|
||||
}, present: { _ in })
|
||||
|
||||
|
|
@ -478,8 +478,8 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
|||
itemNode.listNode.hidePsa = { [weak self] peerId in
|
||||
self?.hidePsa?(peerId)
|
||||
}
|
||||
itemNode.listNode.deletePeerChat = { [weak self] peerId in
|
||||
self?.deletePeerChat?(peerId)
|
||||
itemNode.listNode.deletePeerChat = { [weak self] peerId, joined in
|
||||
self?.deletePeerChat?(peerId, joined)
|
||||
}
|
||||
itemNode.listNode.peerSelected = { [weak self] peerId, a, b in
|
||||
self?.peerSelected?(peerId, a, b)
|
||||
|
|
@ -527,7 +527,7 @@ final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
|||
var present: ((ViewController) -> Void)?
|
||||
var toggleArchivedFolderHiddenByDefault: (() -> Void)?
|
||||
var hidePsa: ((PeerId) -> Void)?
|
||||
var deletePeerChat: ((PeerId) -> Void)?
|
||||
var deletePeerChat: ((PeerId, Bool) -> Void)?
|
||||
var peerSelected: ((Peer, Bool, ChatListNodeEntryPromoInfo?) -> Void)?
|
||||
var groupSelected: ((PeerGroupId) -> Void)?
|
||||
var updatePeerGrouping: ((PeerId, Bool) -> Void)?
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ final class ChatListEmptyNode: ASDisplayNode {
|
|||
animationName = "ChatListEmpty"
|
||||
}
|
||||
if let path = getAppBundle().path(forResource: animationName, ofType: "tgs") {
|
||||
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 248, height: 248, playbackMode: .once, mode: .direct)
|
||||
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 248, height: 248, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
||||
self.animationSize = CGSize(width: 124.0, height: 124.0)
|
||||
self.animationNode.visibility = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ class ChatListFilterSettingsHeaderItemNode: ListViewItemNode {
|
|||
animationName = "ChatListNewFolder"
|
||||
}
|
||||
if let path = getAppBundle().path(forResource: animationName, ofType: "tgs") {
|
||||
strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .once, mode: .direct)
|
||||
strongSelf.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
|
||||
strongSelf.animationNode.visibility = true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1028,7 +1028,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
|
|||
}
|
||||
}, setItemPinned: { _, _ in
|
||||
}, setPeerMuted: { _, _ in
|
||||
}, deletePeer: { _ in
|
||||
}, deletePeer: { _, _ in
|
||||
}, updatePeerGrouping: { _, _ in
|
||||
}, togglePeerMarkedUnread: { _, _ in
|
||||
}, toggleArchivedFolderHiddenByDefault: {
|
||||
|
|
|
|||
|
|
@ -1306,6 +1306,10 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
let (size, apply) = inputActivitiesLayout(CGSize(width: rawContentWidth - badgeSize, height: 40.0), item.presentationData, item.presentationData.theme.chatList.messageTextColor, item.index.messageIndex.id.peerId, inputActivities)
|
||||
inputActivitiesSize = size
|
||||
inputActivitiesApply = apply
|
||||
} else {
|
||||
let (size, apply) = inputActivitiesLayout(CGSize(width: rawContentWidth - badgeSize, height: 40.0), item.presentationData, item.presentationData.theme.chatList.messageTextColor, item.index.messageIndex.id.peerId, [])
|
||||
inputActivitiesSize = size
|
||||
inputActivitiesApply = apply
|
||||
}
|
||||
|
||||
var online = false
|
||||
|
|
@ -1946,7 +1950,15 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
item.interaction.setPeerMuted(item.index.messageIndex.id.peerId, false)
|
||||
close = false
|
||||
case RevealOptionKey.delete.rawValue:
|
||||
item.interaction.deletePeer(item.index.messageIndex.id.peerId)
|
||||
var joined = false
|
||||
if case let .peer(messages, _, _, _, _, _, _, _, _, _, _, _) = item.content, let message = messages.first {
|
||||
for media in message.media {
|
||||
if let action = media as? TelegramMediaAction, action.action == .peerJoined {
|
||||
joined = true
|
||||
}
|
||||
}
|
||||
}
|
||||
item.interaction.deletePeer(item.index.messageIndex.id.peerId, joined)
|
||||
case RevealOptionKey.archive.rawValue:
|
||||
item.interaction.updatePeerGrouping(item.index.messageIndex.id.peerId, true)
|
||||
close = false
|
||||
|
|
|
|||
|
|
@ -201,9 +201,25 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
|
|||
if let discardReason = discardReason {
|
||||
switch discardReason {
|
||||
case .busy, .disconnect:
|
||||
messageText = strings.Notification_CallCanceled
|
||||
if isVideo {
|
||||
messageText = strings.Notification_VideoCallCanceled
|
||||
} else {
|
||||
messageText = strings.Notification_CallCanceled
|
||||
}
|
||||
case .missed:
|
||||
messageText = incoming ? strings.Notification_CallMissed : strings.Notification_CallCanceled
|
||||
if incoming {
|
||||
if isVideo {
|
||||
messageText = strings.Notification_VideoCallMissed
|
||||
} else {
|
||||
messageText = strings.Notification_CallMissed
|
||||
}
|
||||
} else {
|
||||
if isVideo {
|
||||
messageText = strings.Notification_VideoCallCanceled
|
||||
} else {
|
||||
messageText = strings.Notification_CallCanceled
|
||||
}
|
||||
}
|
||||
case .hangup:
|
||||
break
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ public final class ChatListNodeInteraction {
|
|||
let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void
|
||||
let setItemPinned: (PinnedItemId, Bool) -> Void
|
||||
let setPeerMuted: (PeerId, Bool) -> Void
|
||||
let deletePeer: (PeerId) -> Void
|
||||
let deletePeer: (PeerId, Bool) -> Void
|
||||
let updatePeerGrouping: (PeerId, Bool) -> Void
|
||||
let togglePeerMarkedUnread: (PeerId, Bool) -> Void
|
||||
let toggleArchivedFolderHiddenByDefault: () -> Void
|
||||
|
|
@ -70,7 +70,7 @@ public final class ChatListNodeInteraction {
|
|||
public var searchTextHighightState: String?
|
||||
var highlightedChatLocation: ChatListHighlightedLocation?
|
||||
|
||||
public init(activateSearch: @escaping () -> Void, peerSelected: @escaping (Peer, ChatListNodeEntryPromoInfo?) -> Void, disabledPeerSelected: @escaping (Peer) -> Void, togglePeerSelected: @escaping (PeerId) -> Void, additionalCategorySelected: @escaping (Int) -> Void, messageSelected: @escaping (Peer, Message, ChatListNodeEntryPromoInfo?) -> Void, groupSelected: @escaping (PeerGroupId) -> Void, addContact: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, setItemPinned: @escaping (PinnedItemId, Bool) -> Void, setPeerMuted: @escaping (PeerId, Bool) -> Void, deletePeer: @escaping (PeerId) -> Void, updatePeerGrouping: @escaping (PeerId, Bool) -> Void, togglePeerMarkedUnread: @escaping (PeerId, Bool) -> Void, toggleArchivedFolderHiddenByDefault: @escaping () -> Void, hidePsa: @escaping (PeerId) -> Void, activateChatPreview: @escaping (ChatListItem, ASDisplayNode, ContextGesture?) -> Void, present: @escaping (ViewController) -> Void) {
|
||||
public init(activateSearch: @escaping () -> Void, peerSelected: @escaping (Peer, ChatListNodeEntryPromoInfo?) -> Void, disabledPeerSelected: @escaping (Peer) -> Void, togglePeerSelected: @escaping (PeerId) -> Void, additionalCategorySelected: @escaping (Int) -> Void, messageSelected: @escaping (Peer, Message, ChatListNodeEntryPromoInfo?) -> Void, groupSelected: @escaping (PeerGroupId) -> Void, addContact: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, setItemPinned: @escaping (PinnedItemId, Bool) -> Void, setPeerMuted: @escaping (PeerId, Bool) -> Void, deletePeer: @escaping (PeerId, Bool) -> Void, updatePeerGrouping: @escaping (PeerId, Bool) -> Void, togglePeerMarkedUnread: @escaping (PeerId, Bool) -> Void, toggleArchivedFolderHiddenByDefault: @escaping () -> Void, hidePsa: @escaping (PeerId) -> Void, activateChatPreview: @escaping (ChatListItem, ASDisplayNode, ContextGesture?) -> Void, present: @escaping (ViewController) -> Void) {
|
||||
self.activateSearch = activateSearch
|
||||
self.peerSelected = peerSelected
|
||||
self.disabledPeerSelected = disabledPeerSelected
|
||||
|
|
@ -430,7 +430,7 @@ public final class ChatListNode: ListView {
|
|||
public var groupSelected: ((PeerGroupId) -> Void)?
|
||||
public var addContact: ((String) -> Void)?
|
||||
public var activateSearch: (() -> Void)?
|
||||
public var deletePeerChat: ((PeerId) -> Void)?
|
||||
public var deletePeerChat: ((PeerId, Bool) -> Void)?
|
||||
public var updatePeerGrouping: ((PeerId, Bool) -> Void)?
|
||||
public var presentAlert: ((String) -> Void)?
|
||||
public var present: ((ViewController) -> Void)?
|
||||
|
|
@ -628,8 +628,8 @@ public final class ChatListNode: ListView {
|
|||
}
|
||||
self?.setCurrentRemovingPeerId(nil)
|
||||
})
|
||||
}, deletePeer: { [weak self] peerId in
|
||||
self?.deletePeerChat?(peerId)
|
||||
}, deletePeer: { [weak self] peerId, joined in
|
||||
self?.deletePeerChat?(peerId, joined)
|
||||
}, updatePeerGrouping: { [weak self] peerId, group in
|
||||
self?.updatePeerGrouping?(peerId, group)
|
||||
}, togglePeerMarkedUnread: { [weak self, weak context] peerId, animated in
|
||||
|
|
|
|||
|
|
@ -113,30 +113,47 @@ func contactContextMenuItems(context: AccountContext, peerId: PeerId, contactsCo
|
|||
}
|
||||
var canVideoCall = false
|
||||
if canCall {
|
||||
if context.sharedContext.immediateExperimentalUISettings.videoCalls {
|
||||
canVideoCall = true
|
||||
if let cachedUserData = transaction.getPeerCachedData(peerId: peerId) as? CachedUserData {
|
||||
if cachedUserData.videoCallsAvailable {
|
||||
canVideoCall = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if canCall {
|
||||
items.append(.action(ContextMenuActionItem(text: strings.ContactList_Context_Call, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Call"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
if let contactsController = contactsController {
|
||||
let callResult = context.sharedContext.callManager?.requestCall(account: context.account, peerId: peerId, isVideo: false, endCurrentIfAny: false)
|
||||
let callResult = context.sharedContext.callManager?.requestCall(context: context, peerId: peerId, isVideo: false, endCurrentIfAny: false)
|
||||
if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
|
||||
if currentPeerId == peerId {
|
||||
context.sharedContext.navigateToCurrentCall()
|
||||
} else {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let _ = (context.account.postbox.transaction { transaction -> (Peer?, Peer?) in
|
||||
return (transaction.getPeer(peerId), currentPeerId.flatMap(transaction.getPeer))
|
||||
} |> deliverOnMainQueue).start(next: { [weak contactsController] peer, current in
|
||||
if let contactsController = contactsController, let peer = peer {
|
||||
if let current = current {
|
||||
contactsController.present(textAlertController(context: context, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
|
||||
let _ = context.sharedContext.callManager?.requestCall(context: context, peerId: peerId, isVideo: false, endCurrentIfAny: true)
|
||||
})]), in: .window(.root))
|
||||
} else {
|
||||
contactsController.present(textAlertController(context: context, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_ExternalCallInProgressMessage, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
|
||||
})]), in: .window(.root))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/*let _ = (context.account.postbox.transaction { transaction -> (Peer?, Peer?) in
|
||||
return (transaction.getPeer(peerId), transaction.getPeer(currentPeerId))
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { [weak contactsController] peer, current in
|
||||
if let contactsController = contactsController, let peer = peer, let current = current {
|
||||
contactsController.present(textAlertController(context: context, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
|
||||
let _ = context.sharedContext.callManager?.requestCall(account: context.account, peerId: peerId, isVideo: false, endCurrentIfAny: true)
|
||||
let _ = context.sharedContext.callManager?.requestCall(context: context, peerId: peerId, isVideo: false, endCurrentIfAny: true)
|
||||
})]), in: .window(.root))
|
||||
}
|
||||
})
|
||||
})*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -144,25 +161,39 @@ func contactContextMenuItems(context: AccountContext, peerId: PeerId, contactsCo
|
|||
})))
|
||||
}
|
||||
if canVideoCall {
|
||||
//TODO:localize
|
||||
items.append(.action(ContextMenuActionItem(text: "Video Call", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Call"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
items.append(.action(ContextMenuActionItem(text: strings.ContactList_Context_VideoCall, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Call"), color: theme.contextMenu.primaryColor) }, action: { _, f in
|
||||
if let contactsController = contactsController {
|
||||
let callResult = context.sharedContext.callManager?.requestCall(account: context.account, peerId: peerId, isVideo: true, endCurrentIfAny: false)
|
||||
let callResult = context.sharedContext.callManager?.requestCall(context: context, peerId: peerId, isVideo: true, endCurrentIfAny: false)
|
||||
if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
|
||||
if currentPeerId == peerId {
|
||||
context.sharedContext.navigateToCurrentCall()
|
||||
} else {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let _ = (context.account.postbox.transaction { transaction -> (Peer?, Peer?) in
|
||||
return (transaction.getPeer(peerId), currentPeerId.flatMap(transaction.getPeer))
|
||||
} |> deliverOnMainQueue).start(next: { [weak contactsController] peer, current in
|
||||
if let contactsController = contactsController, let peer = peer {
|
||||
if let current = current {
|
||||
contactsController.present(textAlertController(context: context, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
|
||||
let _ = context.sharedContext.callManager?.requestCall(context: context, peerId: peerId, isVideo: true, endCurrentIfAny: true)
|
||||
})]), in: .window(.root))
|
||||
} else {
|
||||
contactsController.present(textAlertController(context: context, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_ExternalCallInProgressMessage, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
|
||||
})]), in: .window(.root))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/*let _ = (context.account.postbox.transaction { transaction -> (Peer?, Peer?) in
|
||||
return (transaction.getPeer(peerId), transaction.getPeer(currentPeerId))
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { [weak contactsController] peer, current in
|
||||
if let contactsController = contactsController, let peer = peer, let current = current {
|
||||
contactsController.present(textAlertController(context: context, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
|
||||
let _ = context.sharedContext.callManager?.requestCall(account: context.account, peerId: peerId, isVideo: true, endCurrentIfAny: true)
|
||||
let _ = context.sharedContext.callManager?.requestCall(context: context, peerId: peerId, isVideo: true, endCurrentIfAny: true)
|
||||
})]), in: .window(.root))
|
||||
}
|
||||
})
|
||||
})*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,12 +101,12 @@ private final class ContactListNodeInteraction {
|
|||
fileprivate let openSortMenu: () -> Void
|
||||
fileprivate let authorize: () -> Void
|
||||
fileprivate let suppressWarning: () -> Void
|
||||
fileprivate let openPeer: (ContactListPeer) -> Void
|
||||
fileprivate let openPeer: (ContactListPeer, ContactListAction) -> Void
|
||||
fileprivate let contextAction: ((Peer, ASDisplayNode, ContextGesture?) -> Void)?
|
||||
|
||||
let itemHighlighting = ContactItemHighlighting()
|
||||
|
||||
init(activateSearch: @escaping () -> Void, openSortMenu: @escaping () -> Void, authorize: @escaping () -> Void, suppressWarning: @escaping () -> Void, openPeer: @escaping (ContactListPeer) -> Void, contextAction: ((Peer, ASDisplayNode, ContextGesture?) -> Void)?) {
|
||||
init(activateSearch: @escaping () -> Void, openSortMenu: @escaping () -> Void, authorize: @escaping () -> Void, suppressWarning: @escaping () -> Void, openPeer: @escaping (ContactListPeer, ContactListAction) -> Void, contextAction: ((Peer, ASDisplayNode, ContextGesture?) -> Void)?) {
|
||||
self.activateSearch = activateSearch
|
||||
self.openSortMenu = openSortMenu
|
||||
self.authorize = authorize
|
||||
|
|
@ -128,7 +128,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
|
|||
case permissionInfo(PresentationTheme, String, String, Bool)
|
||||
case permissionEnable(PresentationTheme, String)
|
||||
case option(Int, ContactListAdditionalOption, ListViewItemHeader?, PresentationTheme, PresentationStrings)
|
||||
case peer(Int, ContactListPeer, PeerPresence?, ListViewItemHeader?, ContactsPeerItemSelection, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PresentationPersonNameOrder, Bool)
|
||||
case peer(Int, ContactListPeer, PeerPresence?, ListViewItemHeader?, ContactsPeerItemSelection, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, PresentationPersonNameOrder, Bool, Bool)
|
||||
|
||||
var stableId: ContactListNodeEntryId {
|
||||
switch self {
|
||||
|
|
@ -142,7 +142,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
|
|||
return .permission(action: true)
|
||||
case let .option(index, _, _, _, _):
|
||||
return .option(index: index)
|
||||
case let .peer(_, peer, _, _, _, _, _, _, _, _, _):
|
||||
case let .peer(_, peer, _, _, _, _, _, _, _, _, _, _):
|
||||
switch peer {
|
||||
case let .peer(peer, _, _):
|
||||
return .peerId(peer.id.toInt64())
|
||||
|
|
@ -176,7 +176,7 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
|
|||
})
|
||||
case let .option(_, option, header, theme, _):
|
||||
return ContactListActionItem(presentationData: ItemListPresentationData(presentationData), title: option.title, icon: option.icon, clearHighlightAutomatically: false, header: header, action: option.action)
|
||||
case let .peer(_, peer, presence, header, selection, theme, strings, dateTimeFormat, nameSortOrder, nameDisplayOrder, enabled):
|
||||
case let .peer(_, peer, presence, header, selection, theme, strings, dateTimeFormat, nameSortOrder, nameDisplayOrder, displayCallIcons, enabled):
|
||||
var status: ContactsPeerItemStatus
|
||||
let itemPeer: ContactsPeerItemPeer
|
||||
var isContextActionEnabled = false
|
||||
|
|
@ -230,8 +230,18 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
|
|||
}
|
||||
}
|
||||
}
|
||||
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: isSearch ? .generalSearch : .peer, peer: itemPeer, status: status, enabled: enabled, selection: selection, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in
|
||||
interaction.openPeer(peer)
|
||||
|
||||
var additionalActions: [ContactsPeerItemAction] = []
|
||||
if displayCallIcons {
|
||||
additionalActions = [ContactsPeerItemAction(icon: .voiceCall, action: { _ in
|
||||
interaction.openPeer(peer, .voiceCall)
|
||||
}), ContactsPeerItemAction(icon: .videoCall, action: { _ in
|
||||
interaction.openPeer(peer, .videoCall)
|
||||
})]
|
||||
}
|
||||
|
||||
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: isSearch ? .generalSearch : .peer, peer: itemPeer, status: status, enabled: enabled, selection: selection, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), additionalActions: additionalActions, index: nil, header: header, action: { _ in
|
||||
interaction.openPeer(peer, .generic)
|
||||
}, itemHighlighting: interaction.itemHighlighting, contextAction: itemContextAction)
|
||||
}
|
||||
}
|
||||
|
|
@ -268,9 +278,9 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
|
|||
} else {
|
||||
return false
|
||||
}
|
||||
case let .peer(lhsIndex, lhsPeer, lhsPresence, lhsHeader, lhsSelection, lhsTheme, lhsStrings, lhsTimeFormat, lhsSortOrder, lhsDisplayOrder, lhsEnabled):
|
||||
case let .peer(lhsIndex, lhsPeer, lhsPresence, lhsHeader, lhsSelection, lhsTheme, lhsStrings, lhsTimeFormat, lhsSortOrder, lhsDisplayOrder, lhsDisplayCallIcons, lhsEnabled):
|
||||
switch rhs {
|
||||
case let .peer(rhsIndex, rhsPeer, rhsPresence, rhsHeader, rhsSelection, rhsTheme, rhsStrings, rhsTimeFormat, rhsSortOrder, rhsDisplayOrder, rhsEnabled):
|
||||
case let .peer(rhsIndex, rhsPeer, rhsPresence, rhsHeader, rhsSelection, rhsTheme, rhsStrings, rhsTimeFormat, rhsSortOrder, rhsDisplayOrder, rhsDisplayCallIcons, rhsEnabled):
|
||||
if lhsIndex != rhsIndex {
|
||||
return false
|
||||
}
|
||||
|
|
@ -305,6 +315,9 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
|
|||
if lhsDisplayOrder != rhsDisplayOrder {
|
||||
return false
|
||||
}
|
||||
if lhsDisplayCallIcons != rhsDisplayCallIcons {
|
||||
return false
|
||||
}
|
||||
if lhsEnabled != rhsEnabled {
|
||||
return false
|
||||
}
|
||||
|
|
@ -349,11 +362,11 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
|
|||
case .peer:
|
||||
return true
|
||||
}
|
||||
case let .peer(lhsIndex, _, _, _, _, _, _, _, _, _, _):
|
||||
case let .peer(lhsIndex, _, _, _, _, _, _, _, _, _, _, _):
|
||||
switch rhs {
|
||||
case .search, .sort, .permissionInfo, .permissionEnable, .option:
|
||||
return false
|
||||
case let .peer(rhsIndex, _, _, _, _, _, _, _, _, _, _):
|
||||
case let .peer(rhsIndex, _, _, _, _, _, _, _, _, _, _, _):
|
||||
return lhsIndex < rhsIndex
|
||||
}
|
||||
}
|
||||
|
|
@ -426,7 +439,7 @@ private extension PeerIndexNameRepresentation {
|
|||
}
|
||||
}
|
||||
|
||||
private func contactListNodeEntries(accountPeer: Peer?, peers: [ContactListPeer], presences: [PeerId: PeerPresence], presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState?, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, disabledPeerIds:Set<PeerId>, authorizationStatus: AccessType, warningSuppressed: (Bool, Bool), displaySortOptions: Bool) -> [ContactListNodeEntry] {
|
||||
private func contactListNodeEntries(accountPeer: Peer?, peers: [ContactListPeer], presences: [PeerId: PeerPresence], presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState?, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, disabledPeerIds:Set<PeerId>, authorizationStatus: AccessType, warningSuppressed: (Bool, Bool), displaySortOptions: Bool, displayCallIcons: Bool) -> [ContactListNodeEntry] {
|
||||
var entries: [ContactListNodeEntry] = []
|
||||
|
||||
var commonHeader: ListViewItemHeader?
|
||||
|
|
@ -606,7 +619,7 @@ private func contactListNodeEntries(accountPeer: Peer?, peers: [ContactListPeer]
|
|||
default:
|
||||
enabled = true
|
||||
}
|
||||
entries.append(.peer(i, orderedPeers[i], presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, enabled))
|
||||
entries.append(.peer(i, orderedPeers[i], presence, header, selection, theme, strings, dateTimeFormat, sortOrder, displayOrder, displayCallIcons, enabled))
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
|
@ -629,7 +642,7 @@ private func preparedContactListNodeTransition(context: AccountContext, presenta
|
|||
case .search:
|
||||
//indexSections.apend(CollectionIndexNode.searchIndex)
|
||||
break
|
||||
case let .peer(_, _, _, header, _, _, _, _, _, _, _):
|
||||
case let .peer(_, _, _, header, _, _, _, _, _, _, _, _):
|
||||
if let header = header as? ContactListNameIndexHeader {
|
||||
if !existingSections.contains(header.letter) {
|
||||
existingSections.insert(header.letter)
|
||||
|
|
@ -771,7 +784,7 @@ public final class ContactListNode: ASDisplayNode {
|
|||
|
||||
public var activateSearch: (() -> Void)?
|
||||
public var openSortMenu: (() -> Void)?
|
||||
public var openPeer: ((ContactListPeer) -> Void)?
|
||||
public var openPeer: ((ContactListPeer, ContactListAction) -> Void)?
|
||||
public var openPrivacyPolicy: (() -> Void)?
|
||||
public var suppressPermissionWarning: (() -> Void)?
|
||||
private let contextAction: ((Peer, ASDisplayNode, ContextGesture?) -> Void)?
|
||||
|
|
@ -786,7 +799,7 @@ public final class ContactListNode: ASDisplayNode {
|
|||
private var authorizationNode: PermissionContentNode
|
||||
private let displayPermissionPlaceholder: Bool
|
||||
|
||||
public init(context: AccountContext, presentation: Signal<ContactListPresentation, NoError>, filters: [ContactListFilter] = [.excludeSelf], selectionState: ContactListNodeGroupSelectionState? = nil, displayPermissionPlaceholder: Bool = true, displaySortOptions: Bool = false, contextAction: ((Peer, ASDisplayNode, ContextGesture?) -> Void)? = nil, isSearch: Bool = false) {
|
||||
public init(context: AccountContext, presentation: Signal<ContactListPresentation, NoError>, filters: [ContactListFilter] = [.excludeSelf], selectionState: ContactListNodeGroupSelectionState? = nil, displayPermissionPlaceholder: Bool = true, displaySortOptions: Bool = false, displayCallIcons: Bool = false, contextAction: ((Peer, ASDisplayNode, ContextGesture?) -> Void)? = nil, isSearch: Bool = false) {
|
||||
self.context = context
|
||||
self.filters = filters
|
||||
self.displayPermissionPlaceholder = displayPermissionPlaceholder
|
||||
|
|
@ -856,8 +869,8 @@ public final class ContactListNode: ASDisplayNode {
|
|||
authorizeImpl?()
|
||||
}, suppressWarning: { [weak self] in
|
||||
self?.suppressPermissionWarning?()
|
||||
}, openPeer: { [weak self] peer in
|
||||
self?.openPeer?(peer)
|
||||
}, openPeer: { [weak self] peer, action in
|
||||
self?.openPeer?(peer, action)
|
||||
}, contextAction: contextAction)
|
||||
|
||||
self.indexNode.indexSelected = { [weak self] section in
|
||||
|
|
@ -885,7 +898,7 @@ public final class ContactListNode: ASDisplayNode {
|
|||
strongSelf.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.PreferSynchronousDrawing, .PreferSynchronousResourceLoading], scrollToItem: ListViewScrollToItem(index: index, position: .top(-navigationBarSearchContentHeight), animated: false, curve: .Default(duration: nil), directionHint: .Down), additionalScrollDistance: 0.0, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
break loop
|
||||
}
|
||||
case let .peer(_, _, _, header, _, _, _, _, _, _, _):
|
||||
case let .peer(_, _, _, header, _, _, _, _, _, _, _, _):
|
||||
if let header = header as? ContactListNameIndexHeader {
|
||||
if let scalar = UnicodeScalar(header.letter) {
|
||||
let title = "\(Character(scalar))"
|
||||
|
|
@ -1113,7 +1126,7 @@ public final class ContactListNode: ASDisplayNode {
|
|||
peers.append(.deviceContact(stableId, contact.0))
|
||||
}
|
||||
|
||||
let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: localPeersAndStatuses.1, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, authorizationStatus: .allowed, warningSuppressed: (true, true), displaySortOptions: false)
|
||||
let entries = contactListNodeEntries(accountPeer: nil, peers: peers, presences: localPeersAndStatuses.1, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, authorizationStatus: .allowed, warningSuppressed: (true, true), displaySortOptions: false, displayCallIcons: displayCallIcons)
|
||||
let previous = previousEntries.swap(entries)
|
||||
return .single(preparedContactListNodeTransition(context: context, presentationData: presentationData, from: previous ?? [], to: entries, interaction: interaction, firstTime: previous == nil, isEmpty: false, generateIndexSections: generateSections, animation: .none, isSearch: isSearch))
|
||||
}
|
||||
|
|
@ -1191,7 +1204,7 @@ public final class ContactListNode: ASDisplayNode {
|
|||
if (authorizationStatus == .notDetermined || authorizationStatus == .denied) && peers.isEmpty {
|
||||
isEmpty = true
|
||||
}
|
||||
let entries = contactListNodeEntries(accountPeer: view.accountPeer, peers: peers, presences: view.peerPresences, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, authorizationStatus: authorizationStatus, warningSuppressed: warningSuppressed, displaySortOptions: displaySortOptions)
|
||||
let entries = contactListNodeEntries(accountPeer: view.accountPeer, peers: peers, presences: view.peerPresences, presentation: presentation, selectionState: selectionState, theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, disabledPeerIds: disabledPeerIds, authorizationStatus: authorizationStatus, warningSuppressed: warningSuppressed, displaySortOptions: displaySortOptions, displayCallIcons: displayCallIcons)
|
||||
let previous = previousEntries.swap(entries)
|
||||
|
||||
var hadPermissionInfo = false
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ public class ContactsController: ViewController {
|
|||
self?.activateSearch()
|
||||
}
|
||||
|
||||
self.contactsNode.contactListNode.openPeer = { peer in
|
||||
self.contactsNode.contactListNode.openPeer = { peer, _ in
|
||||
openPeer(peer, false)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -75,6 +75,18 @@ public struct ContactsPeerItemBadge {
|
|||
public enum ContactsPeerItemActionIcon {
|
||||
case none
|
||||
case add
|
||||
case voiceCall
|
||||
case videoCall
|
||||
}
|
||||
|
||||
public struct ContactsPeerItemAction {
|
||||
public let icon: ContactsPeerItemActionIcon
|
||||
public let action: ((ContactsPeerItemPeer) -> Void)?
|
||||
|
||||
public init(icon: ContactsPeerItemActionIcon, action: @escaping (ContactsPeerItemPeer) -> Void) {
|
||||
self.icon = icon
|
||||
self.action = action
|
||||
}
|
||||
}
|
||||
|
||||
public enum ContactsPeerItemPeer: Equatable {
|
||||
|
|
@ -120,6 +132,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
|
|||
let selection: ContactsPeerItemSelection
|
||||
let editing: ContactsPeerItemEditing
|
||||
let options: [ItemListPeerItemRevealOption]
|
||||
let additionalActions: [ContactsPeerItemAction]
|
||||
let actionIcon: ContactsPeerItemActionIcon
|
||||
let action: (ContactsPeerItemPeer) -> Void
|
||||
let disabledAction: ((ContactsPeerItemPeer) -> Void)?
|
||||
|
|
@ -134,7 +147,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
|
|||
|
||||
public let header: ListViewItemHeader?
|
||||
|
||||
public init(presentationData: ItemListPresentationData, style: ItemListStyle = .plain, sectionId: ItemListSectionId = 0, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, context: AccountContext, peerMode: ContactsPeerItemPeerMode, peer: ContactsPeerItemPeer, status: ContactsPeerItemStatus, badge: ContactsPeerItemBadge? = nil, enabled: Bool, selection: ContactsPeerItemSelection, editing: ContactsPeerItemEditing, options: [ItemListPeerItemRevealOption] = [], actionIcon: ContactsPeerItemActionIcon = .none, index: PeerNameIndex?, header: ListViewItemHeader?, action: @escaping (ContactsPeerItemPeer) -> Void, disabledAction: ((ContactsPeerItemPeer) -> Void)? = nil, setPeerIdWithRevealedOptions: ((PeerId?, PeerId?) -> Void)? = nil, deletePeer: ((PeerId) -> Void)? = nil, itemHighlighting: ContactItemHighlighting? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil) {
|
||||
public init(presentationData: ItemListPresentationData, style: ItemListStyle = .plain, sectionId: ItemListSectionId = 0, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, context: AccountContext, peerMode: ContactsPeerItemPeerMode, peer: ContactsPeerItemPeer, status: ContactsPeerItemStatus, badge: ContactsPeerItemBadge? = nil, enabled: Bool, selection: ContactsPeerItemSelection, editing: ContactsPeerItemEditing, options: [ItemListPeerItemRevealOption] = [], additionalActions: [ContactsPeerItemAction] = [], actionIcon: ContactsPeerItemActionIcon = .none, index: PeerNameIndex?, header: ListViewItemHeader?, action: @escaping (ContactsPeerItemPeer) -> Void, disabledAction: ((ContactsPeerItemPeer) -> Void)? = nil, setPeerIdWithRevealedOptions: ((PeerId?, PeerId?) -> Void)? = nil, deletePeer: ((PeerId) -> Void)? = nil, itemHighlighting: ContactItemHighlighting? = nil, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? = nil) {
|
||||
self.presentationData = presentationData
|
||||
self.style = style
|
||||
self.sectionId = sectionId
|
||||
|
|
@ -149,6 +162,7 @@ public class ContactsPeerItem: ItemListItem, ListViewItemWithHeader {
|
|||
self.selection = selection
|
||||
self.editing = editing
|
||||
self.options = options
|
||||
self.additionalActions = additionalActions
|
||||
self.actionIcon = actionIcon
|
||||
self.action = action
|
||||
self.disabledAction = disabledAction
|
||||
|
|
@ -303,7 +317,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
|||
private var badgeBackgroundNode: ASImageNode?
|
||||
private var badgeTextNode: TextNode?
|
||||
private var selectionNode: CheckNode?
|
||||
private var actionIconNode: ASImageNode?
|
||||
private var actionButtonNodes: [HighlightableButtonNode]?
|
||||
|
||||
private var isHighlighted: Bool = false
|
||||
|
||||
|
|
@ -325,7 +339,7 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
|||
public var item: ContactsPeerItem? {
|
||||
return self.layoutParams?.0
|
||||
}
|
||||
|
||||
|
||||
required public init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
|
|
@ -489,12 +503,32 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
|||
break
|
||||
}
|
||||
|
||||
let actionIconImage: UIImage?
|
||||
switch item.actionIcon {
|
||||
case .none:
|
||||
actionIconImage = nil
|
||||
case .add:
|
||||
actionIconImage = PresentationResourcesItemList.plusIconImage(item.presentationData.theme)
|
||||
var actionButtons: [ActionButton]?
|
||||
struct ActionButton {
|
||||
let image: UIImage?
|
||||
let action: ((ContactsPeerItemPeer) -> Void)?
|
||||
|
||||
init(theme: PresentationTheme, icon: ContactsPeerItemActionIcon, action: ((ContactsPeerItemPeer) -> Void)?) {
|
||||
let image: UIImage?
|
||||
switch icon {
|
||||
case .none:
|
||||
image = nil
|
||||
case .add:
|
||||
image = PresentationResourcesItemList.plusIconImage(theme)
|
||||
case .voiceCall:
|
||||
image = PresentationResourcesItemList.voiceCallIcon(theme)
|
||||
case .videoCall:
|
||||
image = PresentationResourcesItemList.videoCallIcon(theme)
|
||||
}
|
||||
self.image = image
|
||||
self.action = action
|
||||
}
|
||||
}
|
||||
|
||||
if item.actionIcon != .none {
|
||||
actionButtons = [ActionButton(theme: item.presentationData.theme, icon: item.actionIcon, action: nil)]
|
||||
} else if !item.additionalActions.isEmpty {
|
||||
actionButtons = item.additionalActions.map { ActionButton(theme: item.presentationData.theme, icon: $0.icon, action: $0.action) }
|
||||
}
|
||||
|
||||
var titleAttributedString: NSAttributedString?
|
||||
|
|
@ -620,8 +654,13 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
|||
if let verificationIconImage = verificationIconImage {
|
||||
additionalTitleInset += 3.0 + verificationIconImage.size.width
|
||||
}
|
||||
if let actionIconImage = actionIconImage {
|
||||
additionalTitleInset += 3.0 + actionIconImage.size.width
|
||||
if let actionButtons = actionButtons {
|
||||
additionalTitleInset += 3.0
|
||||
for actionButton in actionButtons {
|
||||
if let image = actionButton.image {
|
||||
additionalTitleInset += image.size.width + 12.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
additionalTitleInset += badgeSize
|
||||
|
|
@ -784,23 +823,37 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
|||
verificationIconNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
if let actionIconImage = actionIconImage {
|
||||
if strongSelf.actionIconNode == nil {
|
||||
let actionIconNode = ASImageNode()
|
||||
actionIconNode.isLayerBacked = true
|
||||
actionIconNode.displayWithoutProcessing = true
|
||||
actionIconNode.displaysAsynchronously = false
|
||||
strongSelf.actionIconNode = actionIconNode
|
||||
strongSelf.containerNode.addSubnode(actionIconNode)
|
||||
if let actionButtons = actionButtons {
|
||||
if strongSelf.actionButtonNodes == nil {
|
||||
var actionButtonNodes: [HighlightableButtonNode] = []
|
||||
for action in actionButtons {
|
||||
let actionButtonNode = HighlightableButtonNode()
|
||||
actionButtonNode.isUserInteractionEnabled = action.action != nil
|
||||
actionButtonNode.addTarget(strongSelf, action: #selector(strongSelf.actionButtonPressed(_:)), forControlEvents: .touchUpInside)
|
||||
strongSelf.containerNode.addSubnode(actionButtonNode)
|
||||
|
||||
actionButtonNodes.append(actionButtonNode)
|
||||
}
|
||||
strongSelf.actionButtonNodes = actionButtonNodes
|
||||
}
|
||||
if let actionIconNode = strongSelf.actionIconNode {
|
||||
actionIconNode.image = actionIconImage
|
||||
|
||||
transition.updateFrame(node: actionIconNode, frame: CGRect(origin: CGPoint(x: revealOffset + params.width - params.rightInset - 12.0 - actionIconImage.size.width, y: floor((nodeLayout.contentSize.height - actionIconImage.size.height) / 2.0)), size: actionIconImage.size))
|
||||
if let actionButtonNodes = strongSelf.actionButtonNodes {
|
||||
var offset: CGFloat = 0.0
|
||||
if actionButtons.count > 1 {
|
||||
offset += 12.0
|
||||
}
|
||||
for (actionButtonNode, actionButton) in zip(actionButtonNodes, actionButtons).reversed() {
|
||||
guard let actionButtonImage = actionButton.image else {
|
||||
continue
|
||||
}
|
||||
actionButtonNode.setImage(actionButton.image, for: .normal)
|
||||
transition.updateFrame(node: actionButtonNode, frame: CGRect(origin: CGPoint(x: revealOffset + params.width - params.rightInset - 12.0 - actionButtonImage.size.width - offset, y: floor((nodeLayout.contentSize.height - actionButtonImage.size.height) / 2.0)), size: actionButtonImage.size))
|
||||
|
||||
offset += actionButtonImage.size.width + 12.0
|
||||
}
|
||||
}
|
||||
} else if let actionIconNode = strongSelf.actionIconNode {
|
||||
strongSelf.actionIconNode = nil
|
||||
actionIconNode.removeFromSupernode()
|
||||
} else if let actionButtonNodes = strongSelf.actionButtonNodes {
|
||||
strongSelf.actionButtonNodes = nil
|
||||
actionButtonNodes.forEach { $0.removeFromSupernode() }
|
||||
}
|
||||
|
||||
let badgeBackgroundWidth: CGFloat
|
||||
|
|
@ -893,6 +946,13 @@ public class ContactsPeerItemNode: ItemListRevealOptionsItemNode {
|
|||
}
|
||||
}
|
||||
|
||||
@objc private func actionButtonPressed(_ sender: HighlightableButtonNode) {
|
||||
guard let actionButtonNodes = self.actionButtonNodes, let index = actionButtonNodes.firstIndex(of: sender), let item = self.item, index < item.additionalActions.count else {
|
||||
return
|
||||
}
|
||||
item.additionalActions[index].action?(item.peer)
|
||||
}
|
||||
|
||||
override public func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
super.updateRevealOffset(offset: offset, transition: transition)
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ import TelegramPresentationData
|
|||
import LegacyComponents
|
||||
import AccountContext
|
||||
|
||||
public enum DeviceAccessCameraSubject {
|
||||
case video
|
||||
case videoCall
|
||||
}
|
||||
|
||||
|
||||
public enum DeviceAccessMicrophoneSubject {
|
||||
case audio
|
||||
case video
|
||||
|
|
@ -34,7 +40,7 @@ public enum DeviceAccessLocationSubject {
|
|||
}
|
||||
|
||||
public enum DeviceAccessSubject {
|
||||
case camera
|
||||
case camera(DeviceAccessCameraSubject)
|
||||
case microphone(DeviceAccessMicrophoneSubject)
|
||||
case mediaLibrary(DeviceAccessMediaLibrarySubject)
|
||||
case location(DeviceAccessLocationSubject)
|
||||
|
|
@ -246,14 +252,20 @@ public final class DeviceAccess {
|
|||
|
||||
public static func authorizeAccess(to subject: DeviceAccessSubject, registerForNotifications: ((@escaping (Bool) -> Void) -> Void)? = nil, requestSiriAuthorization: ((@escaping (Bool) -> Void) -> Void)? = nil, locationManager: LocationManager? = nil, presentationData: PresentationData? = nil, present: @escaping (ViewController, Any?) -> Void = { _, _ in }, openSettings: @escaping () -> Void = { }, displayNotificationFromBackground: @escaping (String) -> Void = { _ in }, _ completion: @escaping (Bool) -> Void = { _ in }) {
|
||||
switch subject {
|
||||
case .camera:
|
||||
case let .camera(cameraSubject):
|
||||
let status = PGCamera.cameraAuthorizationStatus()
|
||||
if status == PGCameraAuthorizationStatusNotDetermined {
|
||||
AVCaptureDevice.requestAccess(for: AVMediaType.video) { response in
|
||||
Queue.mainQueue().async {
|
||||
completion(response)
|
||||
if !response, let presentationData = presentationData {
|
||||
let text = presentationData.strings.AccessDenied_Camera
|
||||
let text: String
|
||||
switch cameraSubject {
|
||||
case .video:
|
||||
text = presentationData.strings.AccessDenied_Camera
|
||||
case .videoCall:
|
||||
text = presentationData.strings.AccessDenied_VideoCallCamera
|
||||
}
|
||||
present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
|
||||
openSettings()
|
||||
})]), nil)
|
||||
|
|
|
|||
|
|
@ -220,8 +220,8 @@ public extension CALayer {
|
|||
self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "opacity", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: completion)
|
||||
}
|
||||
|
||||
func animateScale(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
|
||||
self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "transform.scale", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: completion)
|
||||
func animateScale(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "transform.scale", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion)
|
||||
}
|
||||
|
||||
func animateScaleY(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
|
||||
|
|
|
|||
|
|
@ -576,7 +576,7 @@ public extension ContainedViewLayoutTransition {
|
|||
}
|
||||
}
|
||||
|
||||
func animateTransformScale(node: ASDisplayNode, from fromScale: CGFloat, completion: ((Bool) -> Void)? = nil) {
|
||||
func animateTransformScale(node: ASDisplayNode, from fromScale: CGFloat, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
let t = node.layer.transform
|
||||
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
|
||||
if currentScale.isEqual(to: fromScale) {
|
||||
|
|
@ -592,7 +592,16 @@ public extension ContainedViewLayoutTransition {
|
|||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
node.layer.animateScale(from: fromScale, to: currentScale, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
|
||||
let calculatedFrom: CGFloat
|
||||
let calculatedTo: CGFloat
|
||||
if additive {
|
||||
calculatedFrom = fromScale - currentScale
|
||||
calculatedTo = 0.0
|
||||
} else {
|
||||
calculatedFrom = fromScale
|
||||
calculatedTo = currentScale
|
||||
}
|
||||
node.layer.animateScale(from: calculatedFrom, to: calculatedTo, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, additive: additive, completion: { result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
|
|
@ -953,6 +962,73 @@ public extension ContainedViewLayoutTransition {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func updateTransformRotation(view: UIView, angle: CGFloat, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
let t = view.layer.transform
|
||||
let currentAngle = atan2(t.m12, t.m11)
|
||||
if currentAngle.isEqual(to: angle) {
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch self {
|
||||
case .immediate:
|
||||
view.layer.transform = CATransform3DMakeRotation(angle, 0.0, 0.0, 1.0)
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
let previousAngle: CGFloat
|
||||
if beginWithCurrentState, let presentation = view.layer.presentation() {
|
||||
let t = presentation.transform
|
||||
previousAngle = atan2(t.m12, t.m11)
|
||||
} else {
|
||||
previousAngle = currentAngle
|
||||
}
|
||||
view.layer.transform = CATransform3DMakeRotation(angle, 0.0, 0.0, 1.0)
|
||||
view.layer.animateRotation(from: previousAngle, to: angle, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func updateTransformRotationAndScale(view: UIView, angle: CGFloat, scale: CGPoint, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
let t = view.layer.transform
|
||||
let currentAngle = atan2(t.m12, t.m11)
|
||||
let currentScale = CGPoint(x: t.m11, y: t.m12)
|
||||
if currentAngle.isEqual(to: angle) && currentScale == scale {
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch self {
|
||||
case .immediate:
|
||||
view.layer.transform = CATransform3DRotate(CATransform3DMakeScale(scale.x, scale.y, 1.0), angle, 0.0, 0.0, 1.0)
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
let previousAngle: CGFloat
|
||||
if beginWithCurrentState, let presentation = view.layer.presentation() {
|
||||
let t = presentation.transform
|
||||
previousAngle = atan2(t.m12, t.m11)
|
||||
} else {
|
||||
previousAngle = currentAngle
|
||||
}
|
||||
view.layer.transform = CATransform3DRotate(CATransform3DMakeScale(scale.x, scale.y, 1.0), angle, 0.0, 0.0, 1.0)
|
||||
view.layer.animateRotation(from: previousAngle, to: angle, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
|
|
|
|||
|
|
@ -383,6 +383,27 @@ public func generateGradientTintedImage(image: UIImage?, colors: [UIColor]) -> U
|
|||
return tintedImage
|
||||
}
|
||||
|
||||
public func generateGradientImage(size: CGSize, colors: [UIColor], locations: [CGFloat]) -> UIImage? {
|
||||
guard colors.count == locations.count else {
|
||||
return nil
|
||||
}
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
|
||||
if let context = UIGraphicsGetCurrentContext() {
|
||||
let gradientColors = colors.map { $0.cgColor } as CFArray
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
|
||||
var locations = locations
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
|
||||
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
|
||||
}
|
||||
|
||||
let image = UIGraphicsGetImageFromCurrentImageContext()!
|
||||
UIGraphicsEndImageContext()
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
public func generateScaledImage(image: UIImage?, size: CGSize, opaque: Bool = true, scale: CGFloat? = nil) -> UIImage? {
|
||||
guard let image = image else {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -184,6 +184,8 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
|||
|
||||
public final var keepMinimalScrollHeightWithTopInset: CGFloat?
|
||||
|
||||
public final var itemNodeHitTest: ((CGPoint) -> Bool)?
|
||||
|
||||
public final var stackFromBottom: Bool = false
|
||||
public final var stackFromBottomInsetItemFactor: CGFloat = 0.0
|
||||
public final var limitHitTestToNodes: Bool = false
|
||||
|
|
@ -3876,67 +3878,73 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
|||
}
|
||||
|
||||
self.touchesPosition = touchesPosition
|
||||
self.selectionTouchLocation = touches.first!.location(in: self.view)
|
||||
|
||||
self.selectionTouchDelayTimer?.invalidate()
|
||||
self.selectionLongTapDelayTimer?.invalidate()
|
||||
self.selectionLongTapDelayTimer = nil
|
||||
let timer = Timer(timeInterval: 0.08, target: ListViewTimerProxy { [weak self] in
|
||||
if let strongSelf = self, strongSelf.selectionTouchLocation != nil {
|
||||
strongSelf.clearHighlightAnimated(false)
|
||||
|
||||
if let index = strongSelf.itemIndexAtPoint(strongSelf.touchesPosition) {
|
||||
var canBeSelectedOrLongTapped = false
|
||||
for itemNode in strongSelf.itemNodes {
|
||||
if itemNode.index == index && (strongSelf.items[index].selectable && itemNode.canBeSelected) || itemNode.canBeLongTapped {
|
||||
canBeSelectedOrLongTapped = true
|
||||
}
|
||||
}
|
||||
var processSelection = true
|
||||
if let itemNodeHitTest = self.itemNodeHitTest, !itemNodeHitTest(touchesPosition) {
|
||||
processSelection = false
|
||||
}
|
||||
|
||||
if processSelection {
|
||||
self.selectionTouchLocation = touches.first!.location(in: self.view)
|
||||
self.selectionTouchDelayTimer?.invalidate()
|
||||
self.selectionLongTapDelayTimer?.invalidate()
|
||||
self.selectionLongTapDelayTimer = nil
|
||||
let timer = Timer(timeInterval: 0.08, target: ListViewTimerProxy { [weak self] in
|
||||
if let strongSelf = self, strongSelf.selectionTouchLocation != nil {
|
||||
strongSelf.clearHighlightAnimated(false)
|
||||
|
||||
if canBeSelectedOrLongTapped {
|
||||
strongSelf.highlightedItemIndex = index
|
||||
if let index = strongSelf.itemIndexAtPoint(strongSelf.touchesPosition) {
|
||||
var canBeSelectedOrLongTapped = false
|
||||
for itemNode in strongSelf.itemNodes {
|
||||
if itemNode.index == index && itemNode.canBeSelected {
|
||||
if true {
|
||||
if !itemNode.isLayerBacked {
|
||||
strongSelf.reorderItemNodeToFront(itemNode)
|
||||
for (_, headerNode) in strongSelf.itemHeaderNodes {
|
||||
strongSelf.reorderHeaderNodeToFront(headerNode)
|
||||
if itemNode.index == index && (strongSelf.items[index].selectable && itemNode.canBeSelected) || itemNode.canBeLongTapped {
|
||||
canBeSelectedOrLongTapped = true
|
||||
}
|
||||
}
|
||||
|
||||
if canBeSelectedOrLongTapped {
|
||||
strongSelf.highlightedItemIndex = index
|
||||
for itemNode in strongSelf.itemNodes {
|
||||
if itemNode.index == index && itemNode.canBeSelected {
|
||||
if true {
|
||||
if !itemNode.isLayerBacked {
|
||||
strongSelf.reorderItemNodeToFront(itemNode)
|
||||
for (_, headerNode) in strongSelf.itemHeaderNodes {
|
||||
strongSelf.reorderHeaderNodeToFront(headerNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
let itemNodeFrame = itemNode.frame
|
||||
let itemNodeBounds = itemNode.bounds
|
||||
if strongSelf.items[index].selectable {
|
||||
itemNode.setHighlighted(true, at: strongSelf.touchesPosition.offsetBy(dx: -itemNodeFrame.minX + itemNodeBounds.minX, dy: -itemNodeFrame.minY + itemNodeBounds.minY), animated: false)
|
||||
}
|
||||
|
||||
if itemNode.canBeLongTapped {
|
||||
let timer = Timer(timeInterval: 0.3, target: ListViewTimerProxy {
|
||||
if let strongSelf = self, strongSelf.highlightedItemIndex == index {
|
||||
for itemNode in strongSelf.itemNodes {
|
||||
if itemNode.index == index && itemNode.canBeLongTapped {
|
||||
itemNode.longTapped()
|
||||
strongSelf.clearHighlightAnimated(true)
|
||||
strongSelf.selectionTouchLocation = nil
|
||||
break
|
||||
let itemNodeFrame = itemNode.frame
|
||||
let itemNodeBounds = itemNode.bounds
|
||||
if strongSelf.items[index].selectable {
|
||||
itemNode.setHighlighted(true, at: strongSelf.touchesPosition.offsetBy(dx: -itemNodeFrame.minX + itemNodeBounds.minX, dy: -itemNodeFrame.minY + itemNodeBounds.minY), animated: false)
|
||||
}
|
||||
|
||||
if itemNode.canBeLongTapped {
|
||||
let timer = Timer(timeInterval: 0.3, target: ListViewTimerProxy {
|
||||
if let strongSelf = self, strongSelf.highlightedItemIndex == index {
|
||||
for itemNode in strongSelf.itemNodes {
|
||||
if itemNode.index == index && itemNode.canBeLongTapped {
|
||||
itemNode.longTapped()
|
||||
strongSelf.clearHighlightAnimated(true)
|
||||
strongSelf.selectionTouchLocation = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, selector: #selector(ListViewTimerProxy.timerEvent), userInfo: nil, repeats: false)
|
||||
strongSelf.selectionLongTapDelayTimer = timer
|
||||
RunLoop.main.add(timer, forMode: RunLoop.Mode.common)
|
||||
}, selector: #selector(ListViewTimerProxy.timerEvent), userInfo: nil, repeats: false)
|
||||
strongSelf.selectionLongTapDelayTimer = timer
|
||||
RunLoop.main.add(timer, forMode: RunLoop.Mode.common)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, selector: #selector(ListViewTimerProxy.timerEvent), userInfo: nil, repeats: false)
|
||||
self.selectionTouchDelayTimer = timer
|
||||
RunLoop.main.add(timer, forMode: RunLoop.Mode.common)
|
||||
|
||||
}, selector: #selector(ListViewTimerProxy.timerEvent), userInfo: nil, repeats: false)
|
||||
self.selectionTouchDelayTimer = timer
|
||||
RunLoop.main.add(timer, forMode: RunLoop.Mode.common)
|
||||
}
|
||||
super.touchesBegan(touches, with: event)
|
||||
|
||||
self.updateScroller(transition: .immediate)
|
||||
|
|
|
|||
|
|
@ -579,6 +579,7 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||
var previousModalContainer: NavigationModalContainer?
|
||||
var visibleModalCount = 0
|
||||
var topModalIsFlat = false
|
||||
var isLandscape = layout.orientation == .landscape
|
||||
var hasVisibleStandaloneModal = false
|
||||
var topModalDismissProgress: CGFloat = 0.0
|
||||
|
||||
|
|
@ -784,7 +785,7 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||
let visibleRootModalDismissProgress: CGFloat
|
||||
var additionalModalFrameProgress: CGFloat
|
||||
if visibleModalCount == 1 {
|
||||
effectiveRootModalDismissProgress = topModalIsFlat ? 1.0 : topModalDismissProgress
|
||||
effectiveRootModalDismissProgress = (topModalIsFlat || isLandscape) ? 1.0 : topModalDismissProgress
|
||||
visibleRootModalDismissProgress = effectiveRootModalDismissProgress
|
||||
additionalModalFrameProgress = 0.0
|
||||
} else if visibleModalCount >= 2 {
|
||||
|
|
@ -851,7 +852,7 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||
}
|
||||
let maxScale: CGFloat
|
||||
let maxOffset: CGFloat
|
||||
if topModalIsFlat {
|
||||
if topModalIsFlat || isLandscape {
|
||||
maxScale = 1.0
|
||||
maxOffset = 0.0
|
||||
} else if visibleModalCount <= 1 {
|
||||
|
|
|
|||
|
|
@ -328,6 +328,7 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes
|
|||
|
||||
self.scrollNode.view.isScrollEnabled = !isStandaloneModal
|
||||
|
||||
let isLandscape = layout.orientation == .landscape
|
||||
let containerLayout: ContainerViewLayout
|
||||
let containerFrame: CGRect
|
||||
let containerScale: CGFloat
|
||||
|
|
@ -336,7 +337,7 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes
|
|||
self.panRecognizer?.isEnabled = true
|
||||
self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25)
|
||||
self.container.clipsToBounds = true
|
||||
if isStandaloneModal {
|
||||
if isStandaloneModal || isLandscape {
|
||||
self.container.cornerRadius = 0.0
|
||||
} else {
|
||||
self.container.cornerRadius = 10.0
|
||||
|
|
@ -351,7 +352,7 @@ final class NavigationModalContainer: ASDisplayNode, UIScrollViewDelegate, UIGes
|
|||
}
|
||||
|
||||
var topInset: CGFloat
|
||||
if isStandaloneModal {
|
||||
if isStandaloneModal || isLandscape {
|
||||
topInset = 0.0
|
||||
containerLayout = layout
|
||||
|
||||
|
|
|
|||
|
|
@ -343,6 +343,25 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGest
|
|||
}
|
||||
|
||||
self.transaction(GalleryPagerTransaction(deleteItems: deleteItems, insertItems: insertItems, updateItems: updateItems, focusOnItem: centralItemIndex, synchronous: synchronous))
|
||||
|
||||
if self.updateOnReplacement {
|
||||
self.items = items
|
||||
|
||||
for i in 0 ..< self.items.count {
|
||||
if let itemNode = self.visibleItemNode(at: i) {
|
||||
self.items[i].updateNode(node: itemNode, synchronous: synchronous)
|
||||
}
|
||||
}
|
||||
for i in (0 ..< self.itemNodes.count).reversed() {
|
||||
let node = self.itemNodes[i]
|
||||
if node.index > self.items.count - 1 {
|
||||
node.removeFromSupernode()
|
||||
self.itemNodes.remove(at: i)
|
||||
}
|
||||
}
|
||||
|
||||
self.updateCentralIndexOffset(transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
public func transaction(_ transaction: GalleryPagerTransaction) {
|
||||
|
|
@ -353,7 +372,7 @@ public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGest
|
|||
updatedItem.item.updateNode(node: itemNode, synchronous: transaction.synchronous)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if !transaction.deleteItems.isEmpty || !transaction.insertItems.isEmpty {
|
||||
let deleteItems = transaction.deleteItems.sorted()
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ private let maxWidth: CGFloat = 75.0
|
|||
|
||||
public protocol GalleryThumbnailItem {
|
||||
func isEqual(to: GalleryThumbnailItem) -> Bool
|
||||
var image: (Signal<(TransformImageArguments) -> DrawingContext?, NoError>, CGSize) { get }
|
||||
func image(synchronous: Bool) -> (Signal<(TransformImageArguments) -> DrawingContext?, NoError>, CGSize)
|
||||
}
|
||||
|
||||
private final class GalleryThumbnailItemNode: ASDisplayNode {
|
||||
|
|
@ -19,19 +19,19 @@ private final class GalleryThumbnailItemNode: ASDisplayNode {
|
|||
|
||||
private let imageSize: CGSize
|
||||
|
||||
init(item: GalleryThumbnailItem) {
|
||||
init(item: GalleryThumbnailItem, synchronous: Bool) {
|
||||
self.imageNode = TransformImageNode()
|
||||
self.imageContainerNode = ASDisplayNode()
|
||||
self.imageContainerNode.clipsToBounds = true
|
||||
self.imageContainerNode.cornerRadius = 2.0
|
||||
let (signal, imageSize) = item.image
|
||||
let (signal, imageSize) = item.image(synchronous: synchronous)
|
||||
self.imageSize = imageSize
|
||||
|
||||
super.init()
|
||||
|
||||
self.imageContainerNode.addSubnode(self.imageNode)
|
||||
self.addSubnode(self.imageContainerNode)
|
||||
self.imageNode.setSignal(signal)
|
||||
self.imageNode.setSignal(signal, attemptSynchronously: synchronous)
|
||||
}
|
||||
|
||||
func updateLayout(height: CGFloat, progress: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
|
|
@ -57,6 +57,7 @@ public final class GalleryThumbnailContainerNode: ASDisplayNode, UIScrollViewDel
|
|||
private var itemNodes: [GalleryThumbnailItemNode] = []
|
||||
private var centralIndexAndProgress: (Int, CGFloat?)?
|
||||
private var currentLayout: CGSize?
|
||||
public var updateSynchronously: Bool = false
|
||||
|
||||
private var isPanning: Bool = false
|
||||
|
||||
|
|
@ -96,7 +97,6 @@ public final class GalleryThumbnailContainerNode: ASDisplayNode, UIScrollViewDel
|
|||
for i in 0 ..< self.items.count {
|
||||
if !self.items[i].isEqual(to: items[i]) {
|
||||
updated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -108,7 +108,7 @@ public final class GalleryThumbnailContainerNode: ASDisplayNode, UIScrollViewDel
|
|||
if let index = self.items.firstIndex(where: { $0.isEqual(to: item) }) {
|
||||
itemNodes.append(self.itemNodes[index])
|
||||
} else {
|
||||
itemNodes.append(GalleryThumbnailItemNode(item: item))
|
||||
itemNodes.append(GalleryThumbnailItemNode(item: item, synchronous: self.updateSynchronously))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ final class ChatMediaGalleryThumbnailItem: GalleryThumbnailItem {
|
|||
}
|
||||
}
|
||||
|
||||
var image: (Signal<(TransformImageArguments) -> DrawingContext?, NoError>, CGSize) {
|
||||
func image(synchronous: Bool) -> (Signal<(TransformImageArguments) -> DrawingContext?, NoError>, CGSize) {
|
||||
switch self.thumbnail {
|
||||
case let .image(imageReference):
|
||||
if let representation = largestImageRepresentation(imageReference.media.representations) {
|
||||
|
|
|
|||
|
|
@ -626,7 +626,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
case .playing:
|
||||
isPaused = false
|
||||
playing = true
|
||||
case let .buffering(_, whilePlaying):
|
||||
case let .buffering(_, whilePlaying, _):
|
||||
initialBuffering = true
|
||||
isPaused = !whilePlaying
|
||||
var isStreaming = false
|
||||
|
|
@ -1121,7 +1121,34 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
let fromTransform: CATransform3D
|
||||
let toTransform: CATransform3D
|
||||
|
||||
if let interactiveMediaNode = node.0 as? GalleryItemTransitionNode, interactiveMediaNode.isAvailableForGalleryTransition(), videoNode.hasAttachedContext {
|
||||
if let instantNode = node.0 as? GalleryItemTransitionNode, instantNode.isAvailableForInstantPageTransition(), videoNode.hasAttachedContext {
|
||||
copyView.removeFromSuperview()
|
||||
|
||||
let previousFrame = videoNode.frame
|
||||
let previousSuperview = videoNode.view.superview
|
||||
addToTransitionSurface(videoNode.view)
|
||||
videoNode.view.superview?.bringSubviewToFront(videoNode.view)
|
||||
|
||||
if let previousSuperview = previousSuperview {
|
||||
videoNode.frame = previousSuperview.convert(previousFrame, to: videoNode.view.superview)
|
||||
transformedSuperFrame = transformedSuperFrame.offsetBy(dx: videoNode.position.x - previousFrame.center.x, dy: videoNode.position.y - previousFrame.center.y)
|
||||
}
|
||||
|
||||
let initialScale: CGFloat = 1.0
|
||||
let targetScale = max(transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height)
|
||||
|
||||
videoNode.backgroundColor = .clear
|
||||
|
||||
let transformScale: CGFloat = initialScale * targetScale
|
||||
fromTransform = CATransform3DScale(videoNode.layer.transform, initialScale, initialScale, 1.0)
|
||||
toTransform = CATransform3DScale(videoNode.layer.transform, transformScale, transformScale, 1.0)
|
||||
|
||||
if videoNode.hasAttachedContext {
|
||||
if self.isPaused || !self.keepSoundOnDismiss {
|
||||
videoNode.continuePlayingWithoutSound()
|
||||
}
|
||||
}
|
||||
} else if let interactiveMediaNode = node.0 as? GalleryItemTransitionNode, interactiveMediaNode.isAvailableForGalleryTransition(), videoNode.hasAttachedContext {
|
||||
copyView.removeFromSuperview()
|
||||
|
||||
let previousFrame = videoNode.frame
|
||||
|
|
@ -1158,33 +1185,6 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
fromTransform = CATransform3DScale(videoNode.layer.transform, initialScale, initialScale, 1.0)
|
||||
toTransform = CATransform3DScale(videoNode.layer.transform, transformScale, transformScale, 1.0)
|
||||
|
||||
if videoNode.hasAttachedContext {
|
||||
if self.isPaused || !self.keepSoundOnDismiss {
|
||||
videoNode.continuePlayingWithoutSound()
|
||||
}
|
||||
}
|
||||
} else if let instantNode = node.0 as? GalleryItemTransitionNode, instantNode.isAvailableForInstantPageTransition(), videoNode.hasAttachedContext {
|
||||
copyView.removeFromSuperview()
|
||||
|
||||
let previousFrame = videoNode.frame
|
||||
let previousSuperview = videoNode.view.superview
|
||||
addToTransitionSurface(videoNode.view)
|
||||
videoNode.view.superview?.bringSubviewToFront(videoNode.view)
|
||||
|
||||
if let previousSuperview = previousSuperview {
|
||||
videoNode.frame = previousSuperview.convert(previousFrame, to: videoNode.view.superview)
|
||||
transformedSuperFrame = transformedSuperFrame.offsetBy(dx: videoNode.position.x - previousFrame.center.x, dy: videoNode.position.y - previousFrame.center.y)
|
||||
}
|
||||
|
||||
let initialScale: CGFloat = 1.0
|
||||
let targetScale = max(transformedFrame.size.width / videoNode.layer.bounds.size.width, transformedFrame.size.height / videoNode.layer.bounds.size.height)
|
||||
|
||||
videoNode.backgroundColor = .clear
|
||||
|
||||
let transformScale: CGFloat = initialScale * targetScale
|
||||
fromTransform = CATransform3DScale(videoNode.layer.transform, initialScale, initialScale, 1.0)
|
||||
toTransform = CATransform3DScale(videoNode.layer.transform, transformScale, transformScale, 1.0)
|
||||
|
||||
if videoNode.hasAttachedContext {
|
||||
if self.isPaused || !self.keepSoundOnDismiss {
|
||||
videoNode.continuePlayingWithoutSound()
|
||||
|
|
|
|||
|
|
@ -62,6 +62,16 @@ final class GameControllerNode: ViewControllerTracingNode {
|
|||
}, name: "performAction")
|
||||
|
||||
configuration.userContentController = userController
|
||||
|
||||
configuration.allowsInlineMediaPlayback = true
|
||||
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
|
||||
configuration.mediaTypesRequiringUserActionForPlayback = []
|
||||
} else if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
|
||||
configuration.requiresUserActionForMediaPlayback = false
|
||||
} else {
|
||||
configuration.mediaPlaybackRequiresUserAction = false
|
||||
}
|
||||
|
||||
let webView = WKWebView(frame: CGRect(), configuration: configuration)
|
||||
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
|
||||
webView.allowsLinkPreview = false
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ public final class HashtagSearchController: TelegramBaseController {
|
|||
}, setPeerIdWithRevealedOptions: { _, _ in
|
||||
}, setItemPinned: { _, _ in
|
||||
}, setPeerMuted: { _, _ in
|
||||
}, deletePeer: { _ in
|
||||
}, deletePeer: { _, _ in
|
||||
}, updatePeerGrouping: { _, _ in
|
||||
}, togglePeerMarkedUnread: { _, _ in
|
||||
}, toggleArchivedFolderHiddenByDefault: {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ static_library(
|
|||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
deps = [
|
||||
"//submodules/MozjpegBinding:MozjpegBinding",
|
||||
],
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||
"$SDKROOT/System/Library/Frameworks/UIKit.framework",
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@ swift_library(
|
|||
name = "ImageCompression",
|
||||
module_name = "ImageCompression",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
deps = [
|
||||
"//submodules/MozjpegBinding:MozjpegBinding",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,8 +1,19 @@
|
|||
import Foundation
|
||||
import AVFoundation
|
||||
import UIKit
|
||||
import MozjpegBinding
|
||||
|
||||
public func extractImageExtraScans(_ data: Data) -> [Int] {
|
||||
return extractJPEGDataScans(data).map { item in
|
||||
return item.intValue
|
||||
}
|
||||
}
|
||||
|
||||
public func compressImageToJPEG(_ image: UIImage, quality: Float) -> Data? {
|
||||
if let result = compressJPEGData(image) {
|
||||
return result
|
||||
}
|
||||
|
||||
let data = NSMutableData()
|
||||
guard let destination = CGImageDestinationCreateWithData(data as CFMutableData, "public.jpeg" as CFString, 1, nil) else {
|
||||
return nil
|
||||
|
|
@ -46,3 +57,7 @@ public func compressImage(_ image: UIImage, quality: Float) -> Data? {
|
|||
|
||||
return data as Data
|
||||
}
|
||||
|
||||
public func compressImageMiniThumbnail(_ image: UIImage) -> Data? {
|
||||
return compressMiniThumbnail(image)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ private struct InstantImageGalleryThumbnailItem: GalleryThumbnailItem {
|
|||
let account: Account
|
||||
let mediaReference: AnyMediaReference
|
||||
|
||||
var image: (Signal<(TransformImageArguments) -> DrawingContext?, NoError>, CGSize) {
|
||||
func image(synchronous: Bool) -> (Signal<(TransformImageArguments) -> DrawingContext?, NoError>, CGSize) {
|
||||
if let imageReferene = mediaReference.concrete(TelegramMediaImage.self), let representation = largestImageRepresentation(imageReferene.media.representations) {
|
||||
return (mediaGridMessagePhoto(account: self.account, photoReference: imageReferene), representation.dimensions.cgSize)
|
||||
} else if let fileReference = mediaReference.concrete(TelegramMediaFile.self), let dimensions = fileReference.media.dimensions {
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ public struct InstantPageGalleryEntry: Equatable {
|
|||
var representations: [TelegramMediaImageRepresentation] = []
|
||||
representations.append(contentsOf: file.previewRepresentations)
|
||||
if let dimensions = file.dimensions {
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: dimensions, resource: file.resource))
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: dimensions, resource: file.resource, progressiveSizes: []))
|
||||
}
|
||||
let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, immediateThumbnailData: file.immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
|
||||
return InstantImageGalleryItem(context: context, presentationData: presentationData, itemId: self.index, imageReference: .webPage(webPage: WebpageReference(webPage), media: image), caption: caption, credit: credit, location: self.location, openUrl: openUrl, openUrlOptions: openUrlOptions)
|
||||
|
|
|
|||
|
|
@ -375,7 +375,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
|
|||
thumbnailItem = .animated(item.file.resource)
|
||||
resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: item.file.resource)
|
||||
} else if let dimensions = item.file.dimensions, let resource = chatMessageStickerResource(file: item.file, small: true) as? TelegramMediaResource {
|
||||
thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource))
|
||||
thumbnailItem = .still(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: []))
|
||||
resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: resource)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -238,7 +238,9 @@ open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||
|
||||
self.listNode = ListView()
|
||||
self.leftOverlayNode = ASDisplayNode()
|
||||
self.leftOverlayNode.isUserInteractionEnabled = false
|
||||
self.rightOverlayNode = ASDisplayNode()
|
||||
self.rightOverlayNode.isUserInteractionEnabled = false
|
||||
|
||||
super.init()
|
||||
|
||||
|
|
@ -302,6 +304,14 @@ open class ItemListControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||
let _ = strongSelf.contentScrollingEnded?(strongSelf.listNode)
|
||||
}
|
||||
}
|
||||
|
||||
self.listNode.itemNodeHitTest = { [weak self] point in
|
||||
if let strongSelf = self {
|
||||
return point.x > strongSelf.leftOverlayNode.frame.maxX && point.x < strongSelf.rightOverlayNode.frame.minX
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
let previousState = Atomic<ItemListNodeState?>(value: nil)
|
||||
self.transitionDisposable.set(((state
|
||||
|
|
|
|||
|
|
@ -22,8 +22,6 @@
|
|||
#import <LegacyComponents/PGCamera.h>
|
||||
#import <LegacyComponents/PGCameraCaptureSession.h>
|
||||
#import <LegacyComponents/PGCameraDeviceAngleSampler.h>
|
||||
#import <LegacyComponents/PGCameraMomentSegment.h>
|
||||
#import <LegacyComponents/PGCameraMomentSession.h>
|
||||
#import <LegacyComponents/PGCameraMovieWriter.h>
|
||||
#import <LegacyComponents/PGCameraShotMetadata.h>
|
||||
#import <LegacyComponents/PGCameraVolumeButtonHandler.h>
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
#import <CoreMedia/CoreMedia.h>
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
@interface PGCameraMomentSegment : NSObject
|
||||
|
||||
@property (nonatomic, readonly) NSURL *fileURL;
|
||||
@property (nonatomic, readonly) AVAsset *asset;
|
||||
@property (nonatomic, readonly) NSTimeInterval duration;
|
||||
|
||||
- (instancetype)initWithURL:(NSURL *)url duration:(NSTimeInterval)duration;
|
||||
|
||||
@end
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
#import <LegacyComponents/PGCameraMomentSegment.h>
|
||||
|
||||
@class PGCamera;
|
||||
|
||||
@interface PGCameraMomentSession : NSObject
|
||||
|
||||
@property (nonatomic, copy) void (^beganCapture)(void);
|
||||
@property (nonatomic, copy) void (^finishedCapture)(void);
|
||||
@property (nonatomic, copy) bool (^captureIsAvailable)(void);
|
||||
@property (nonatomic, copy) void (^durationChanged)(NSTimeInterval);
|
||||
|
||||
@property (nonatomic, readonly) bool isCapturing;
|
||||
@property (nonatomic, readonly) UIImage *previewImage;
|
||||
@property (nonatomic, readonly) bool hasSegments;
|
||||
|
||||
@property (nonatomic, readonly) PGCameraMomentSegment *lastSegment;
|
||||
|
||||
- (instancetype)initWithCamera:(PGCamera *)camera;
|
||||
|
||||
- (void)captureSegment;
|
||||
- (void)commitSegment;
|
||||
|
||||
- (void)addSegment:(PGCameraMomentSegment *)segment;
|
||||
- (void)removeSegment:(PGCameraMomentSegment *)segment;
|
||||
- (void)removeLastSegment;
|
||||
- (void)removeAllSegments;
|
||||
|
||||
@end
|
||||
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
@property (nonatomic, strong) UIImage *image;
|
||||
|
||||
@property (nonatomic, readonly) UIView *clipView;
|
||||
|
||||
@property (nonatomic, readonly) CGSize originalSize;
|
||||
@property (nonatomic, assign) CGRect cropRect;
|
||||
@property (nonatomic, assign) UIImageOrientation cropOrientation;
|
||||
|
|
@ -37,6 +39,7 @@
|
|||
- (void)animateTransitionOut;
|
||||
- (void)transitionInFinishedFromCamera:(bool)fromCamera;
|
||||
|
||||
- (void)attachEntitiesView;
|
||||
- (void)closeCurtains;
|
||||
- (void)openCurtains;
|
||||
|
||||
|
|
|
|||
|
|
@ -85,4 +85,7 @@ typedef enum {
|
|||
|
||||
+ (TGPhotoEditorTab)defaultTabsForAvatarIntent;
|
||||
|
||||
- (NSTimeInterval)currentTime;
|
||||
- (void)setMinimalVideoDuration:(NSTimeInterval)duration;
|
||||
|
||||
@end
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@
|
|||
bool _dismissing;
|
||||
UIView *_transitionView;
|
||||
bool _noTransitionToSnapshot;
|
||||
|
||||
bool _animateScale;
|
||||
}
|
||||
|
||||
@property (nonatomic, weak) id<TGMediaEditableItem> item;
|
||||
|
|
@ -36,6 +38,7 @@
|
|||
|
||||
@property (nonatomic, assign) TGPhotoEditorTab availableTabs;
|
||||
|
||||
@property (nonatomic, assign) TGPhotoEditorTab switchingFromTab;
|
||||
@property (nonatomic, assign) TGPhotoEditorTab switchingToTab;
|
||||
|
||||
- (void)transitionOutSwitching:(bool)switching completion:(void (^)(void))completion;
|
||||
|
|
|
|||
|
|
@ -14,9 +14,11 @@ CGFloat TGRadiansToDegrees(CGFloat radians);
|
|||
|
||||
UIImage *TGPhotoEditorCrop(UIImage *image, UIImage *paintingImage, UIImageOrientation orientation, CGFloat rotation, CGRect rect, bool mirrored, CGSize maxSize, CGSize originalSize, bool shouldResize);
|
||||
UIImage *TGPhotoEditorVideoCrop(UIImage *image, UIImage *paintingImage, UIImageOrientation orientation, CGFloat rotation, CGRect rect, bool mirrored, CGSize maxSize, CGSize originalSize, bool shouldResize, bool useImageSize);
|
||||
UIImage *TGPhotoEditorVideoExtCrop(UIImage *inputImage, UIImage *paintingImage, UIImageOrientation orientation, CGFloat rotation, CGRect rect, bool mirrored, CGSize maxSize, CGSize originalSize, bool shouldResize, bool useImageSize, bool skipImageTransform);
|
||||
UIImage *TGPhotoEditorVideoExtCrop(UIImage *inputImage, UIImage *paintingImage, UIImageOrientation orientation, CGFloat rotation, CGRect rect, bool mirrored, CGSize maxSize, CGSize originalSize, bool shouldResize, bool useImageSize, bool skipImageTransform, bool fillPainting);
|
||||
UIImage *TGPhotoEditorFitImage(UIImage *image, CGSize maxSize);
|
||||
CGSize TGRotatedContentSize(CGSize contentSize, CGFloat rotation);
|
||||
|
||||
UIImage *TGPhotoEditorPaintingCrop(UIImage *paintingImage, UIImageOrientation orientation, CGFloat rotation, CGRect rect, bool mirrored, CGSize maxSize, CGSize originalSize, bool shouldResize, bool useImageSize, bool skipImageTransform);
|
||||
|
||||
UIImageOrientation TGNextCWOrientationForOrientation(UIImageOrientation orientation);
|
||||
UIImageOrientation TGNextCCWOrientationForOrientation(UIImageOrientation orientation);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,13 @@
|
|||
|
||||
@protocol TGPhotoPaintStickerRenderView <NSObject>
|
||||
|
||||
@property (nonatomic, copy) void(^started)(double);
|
||||
|
||||
- (void)setIsVisible:(bool)isVisible;
|
||||
- (void)seekTo:(double)timestamp;
|
||||
- (void)play;
|
||||
- (void)pause;
|
||||
- (void)resetToStart;
|
||||
- (int64_t)documentId;
|
||||
- (UIImage *)image;
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ typedef enum
|
|||
TGMediaVideoConversionPresetCompressedVeryHigh,
|
||||
TGMediaVideoConversionPresetAnimation,
|
||||
TGMediaVideoConversionPresetVideoMessage,
|
||||
TGMediaVideoConversionPresetProfileLow,
|
||||
TGMediaVideoConversionPresetProfile,
|
||||
TGMediaVideoConversionPresetProfileHigh,
|
||||
TGMediaVideoConversionPresetProfileVeryHigh,
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
#import "PGCameraMomentSegment.h"
|
||||
|
||||
@interface PGCameraMomentSegment ()
|
||||
{
|
||||
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation PGCameraMomentSegment
|
||||
|
||||
- (instancetype)initWithURL:(NSURL *)url duration:(NSTimeInterval)duration
|
||||
{
|
||||
self = [super init];
|
||||
if (self != nil)
|
||||
{
|
||||
_fileURL = url;
|
||||
_duration = duration;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
#import "PGCameraMomentSession.h"
|
||||
#import "PGCamera.h"
|
||||
|
||||
@interface PGCameraMomentSession ()
|
||||
{
|
||||
NSString *_uniqueIdentifier;
|
||||
NSURL *_segmentsDirectory;
|
||||
|
||||
PGCamera *_camera;
|
||||
NSMutableArray *_segments;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation PGCameraMomentSession
|
||||
|
||||
- (instancetype)initWithCamera:(PGCamera *)camera
|
||||
{
|
||||
self = [super init];
|
||||
if (self != nil)
|
||||
{
|
||||
_camera = camera;
|
||||
_segments = [[NSMutableArray alloc] init];
|
||||
|
||||
int64_t uniqueId = 0;
|
||||
arc4random_buf(&uniqueId, 8);
|
||||
_uniqueIdentifier = [NSString stringWithFormat:@"%x", (int)arc4random()];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSURL *)segmentsDirectory
|
||||
{
|
||||
if (_segmentsDirectory == nil)
|
||||
_segmentsDirectory = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:_uniqueIdentifier]];
|
||||
|
||||
return _segmentsDirectory;
|
||||
}
|
||||
|
||||
- (void)captureSegment
|
||||
{
|
||||
if (self.isCapturing)
|
||||
return;
|
||||
|
||||
_isCapturing = true;
|
||||
|
||||
if (self.beganCapture != nil)
|
||||
self.beganCapture();
|
||||
|
||||
[_camera startVideoRecordingForMoment:true completion:^(NSURL *resultUrl, __unused CGAffineTransform transform, __unused CGSize dimensions, NSTimeInterval duration, bool success)
|
||||
{
|
||||
if (!success)
|
||||
return;
|
||||
|
||||
_isCapturing = false;
|
||||
|
||||
if (self.finishedCapture != nil)
|
||||
self.finishedCapture();
|
||||
|
||||
PGCameraMomentSegment *segment = [[PGCameraMomentSegment alloc] initWithURL:resultUrl duration:duration];
|
||||
[self addSegment:segment];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)commitSegment
|
||||
{
|
||||
[_camera stopVideoRecording];
|
||||
}
|
||||
|
||||
- (void)addSegment:(PGCameraMomentSegment *)segment
|
||||
{
|
||||
[_segments addObject:segment];
|
||||
}
|
||||
|
||||
- (void)removeSegment:(PGCameraMomentSegment *)segment
|
||||
{
|
||||
[_segments removeObject:segment];
|
||||
}
|
||||
|
||||
- (void)removeLastSegment
|
||||
{
|
||||
[_segments removeLastObject];
|
||||
}
|
||||
|
||||
- (void)removeAllSegments
|
||||
{
|
||||
[_segments removeAllObjects];
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
@ -928,7 +928,8 @@ const NSUInteger TGAttachmentDisplayedAssetLimit = 500;
|
|||
if (paintingImage == nil) {
|
||||
paintingImage = adjustments.paintingData.image;
|
||||
}
|
||||
UIImage *thumbnailImage = TGPhotoEditorVideoExtCrop(resultImage, paintingImage, adjustments.cropOrientation, adjustments.cropRotation, adjustments.cropRect, adjustments.cropMirrored, TGScaleToFill(asset.dimensions, CGSizeMake(800, 800)), adjustments.originalSize, true, true, true);
|
||||
UIImage *croppedPaintingImage = TGPhotoEditorPaintingCrop(paintingImage, adjustments.cropOrientation, adjustments.cropRotation, adjustments.cropRect, adjustments.cropMirrored, resultImage.size, adjustments.originalSize, true, true, false);
|
||||
UIImage *thumbnailImage = TGPhotoEditorVideoExtCrop(resultImage, croppedPaintingImage, adjustments.cropOrientation, adjustments.cropRotation, adjustments.cropRect, adjustments.cropMirrored, TGScaleToFill(asset.dimensions, CGSizeMake(800, 800)), adjustments.originalSize, true, true, true, true);
|
||||
if (thumbnailImage != nil) {
|
||||
previewImage = thumbnailImage;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1751,9 +1751,34 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
|
|||
|
||||
TGDispatchOnMainThread(^
|
||||
{
|
||||
if (strongSelf.finishedWithPhoto != nil)
|
||||
strongSelf.finishedWithPhoto(nil, resultImage, nil, nil, nil, nil);
|
||||
|
||||
if (editorValues.paintingData.hasAnimation) {
|
||||
TGVideoEditAdjustments *adjustments = [TGVideoEditAdjustments editAdjustmentsWithPhotoEditorValues:(PGPhotoEditorValues *)editorValues preset:TGMediaVideoConversionPresetProfileVeryHigh];
|
||||
|
||||
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSString alloc] initWithFormat:@"gifvideo_%x.jpg", (int)arc4random()]];
|
||||
NSData *data = UIImageJPEGRepresentation(resultImage, 0.8);
|
||||
[data writeToFile:filePath atomically:true];
|
||||
|
||||
UIImage *previewImage = resultImage;
|
||||
if ([adjustments cropAppliedForAvatar:false] || adjustments.hasPainting || adjustments.toolsApplied)
|
||||
{
|
||||
UIImage *paintingImage = adjustments.paintingData.stillImage;
|
||||
if (paintingImage == nil) {
|
||||
paintingImage = adjustments.paintingData.image;
|
||||
}
|
||||
UIImage *croppedPaintingImage = TGPhotoEditorPaintingCrop(paintingImage, adjustments.cropOrientation, adjustments.cropRotation, adjustments.cropRect, adjustments.cropMirrored, resultImage.size, adjustments.originalSize, true, true, false);
|
||||
UIImage *thumbnailImage = TGPhotoEditorVideoExtCrop(resultImage, croppedPaintingImage, adjustments.cropOrientation, adjustments.cropRotation, adjustments.cropRect, adjustments.cropMirrored, TGScaleToFill(resultImage.size, CGSizeMake(800, 800)), adjustments.originalSize, true, true, true, true);
|
||||
if (thumbnailImage != nil) {
|
||||
previewImage = thumbnailImage;
|
||||
}
|
||||
}
|
||||
|
||||
if (strongSelf.finishedWithVideo != nil)
|
||||
strongSelf.finishedWithVideo(nil, [NSURL fileURLWithPath:filePath], previewImage, 0, CGSizeZero, adjustments, nil, nil, nil, nil);
|
||||
} else {
|
||||
if (strongSelf.finishedWithPhoto != nil)
|
||||
strongSelf.finishedWithPhoto(nil, resultImage, nil, nil, nil, nil);
|
||||
}
|
||||
|
||||
if (strongSelf.shouldStoreCapturedAssets && [input isKindOfClass:[UIImage class]])
|
||||
{
|
||||
[strongSelf _savePhotoToCameraRollWithOriginalImage:image editedImage:[editorValues toolsApplied] ? resultImage : nil];
|
||||
|
|
@ -2608,7 +2633,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
|
|||
if (paintingImage == nil) {
|
||||
paintingImage = adjustments.paintingData.image;
|
||||
}
|
||||
UIImage *thumbnailImage = TGPhotoEditorVideoExtCrop(image, paintingImage, adjustments.cropOrientation, adjustments.cropRotation, adjustments.cropRect, adjustments.cropMirrored, TGScaleToFill(image.size, CGSizeMake(512, 512)), adjustments.originalSize, true, true, true);
|
||||
UIImage *thumbnailImage = TGPhotoEditorVideoExtCrop(image, paintingImage, adjustments.cropOrientation, adjustments.cropRotation, adjustments.cropRect, adjustments.cropMirrored, TGScaleToFill(image.size, CGSizeMake(512, 512)), adjustments.originalSize, true, true, true, false);
|
||||
if (thumbnailImage != nil) {
|
||||
dict[@"previewImage"] = thumbnailImage;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@
|
|||
bool _throttle;
|
||||
TGLocationPinAnnotationView *_ownLiveLocationView;
|
||||
__weak MKAnnotationView *_userLocationView;
|
||||
|
||||
UIImageView *_headingArrowView;
|
||||
}
|
||||
@end
|
||||
|
||||
|
|
@ -162,6 +164,8 @@
|
|||
[_liveLocationsDisposable dispose];
|
||||
[_reloadDisposable dispose];
|
||||
[_frequentUpdatesDisposable dispose];
|
||||
|
||||
[_locationManager stopUpdatingHeading];
|
||||
}
|
||||
|
||||
- (void)tg_setRightBarButtonItem:(UIBarButtonItem *)barButtonItem action:(bool)action animated:(bool)animated {
|
||||
|
|
@ -438,6 +442,36 @@
|
|||
{
|
||||
[super loadView];
|
||||
|
||||
static UIImage *headingArrowImage = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
|
||||
dispatch_once(&onceToken, ^
|
||||
{
|
||||
UIGraphicsBeginImageContextWithOptions(CGSizeMake(28.0f, 28.0f), false, 0.0f);
|
||||
CGContextRef context = UIGraphicsGetCurrentContext();
|
||||
|
||||
CGContextClearRect(context, CGRectMake(0, 0, 28, 28));
|
||||
|
||||
CGContextSetFillColorWithColor(context, UIColorRGB(0x3393fe).CGColor);
|
||||
|
||||
CGContextMoveToPoint(context, 14, 0);
|
||||
CGContextAddLineToPoint(context, 19, 7);
|
||||
CGContextAddLineToPoint(context, 9, 7);
|
||||
CGContextClosePath(context);
|
||||
CGContextFillPath(context);
|
||||
|
||||
CGContextSetBlendMode(context, kCGBlendModeClear);
|
||||
CGContextFillEllipseInRect(context, CGRectMake(5.0, 5.0, 18.0, 18.0));
|
||||
|
||||
headingArrowImage = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
});
|
||||
|
||||
_headingArrowView = [[UIImageView alloc] init];
|
||||
_headingArrowView.hidden = true;
|
||||
_headingArrowView.frame = CGRectMake(0.0, 0.0, 28.0, 28.0);
|
||||
_headingArrowView.image = headingArrowImage;
|
||||
|
||||
_tableView.scrollsToTop = false;
|
||||
_mapView.tapEnabled = false;
|
||||
|
||||
|
|
@ -495,6 +529,8 @@
|
|||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
[_locationManager startUpdatingHeading];
|
||||
|
||||
if (self.previewMode && !animated)
|
||||
{
|
||||
UIView *contentView = [[_mapView subviews] firstObject];
|
||||
|
|
@ -950,6 +986,9 @@
|
|||
{
|
||||
_userLocationView = view;
|
||||
|
||||
[_userLocationView addSubview:_headingArrowView];
|
||||
_headingArrowView.center = CGPointMake(view.frame.size.width / 2.0, view.frame.size.height / 2.0);
|
||||
|
||||
if (_ownLiveLocationView != nil)
|
||||
{
|
||||
[_userLocationView addSubview:_ownLiveLocationView];
|
||||
|
|
@ -982,6 +1021,14 @@
|
|||
return CLLocationCoordinate2DMake(_locationAttachment.latitude, _locationAttachment.longitude);
|
||||
}
|
||||
|
||||
- (void)locationManager:(CLLocationManager *)manager didUpdateHeading:(CLHeading *)newHeading
|
||||
{
|
||||
if (newHeading != nil) {
|
||||
_headingArrowView.hidden = false;
|
||||
_headingArrowView.transform = CGAffineTransformMakeRotation(newHeading.magneticHeading / 180.0 * M_PI);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
|
||||
|
|
|
|||
|
|
@ -920,7 +920,7 @@
|
|||
if (paintingImage == nil) {
|
||||
paintingImage = adjustments.paintingData.image;
|
||||
}
|
||||
UIImage *thumbnailImage = TGPhotoEditorVideoExtCrop(image, paintingImage, adjustments.cropOrientation, adjustments.cropRotation, adjustments.cropRect, adjustments.cropMirrored, TGScaleToFill(asset.dimensions, CGSizeMake(512, 512)), adjustments.originalSize, true, true, true);
|
||||
UIImage *thumbnailImage = TGPhotoEditorVideoExtCrop(image, paintingImage, adjustments.cropOrientation, adjustments.cropRotation, adjustments.cropRect, adjustments.cropMirrored, TGScaleToFill(asset.dimensions, CGSizeMake(512, 512)), adjustments.originalSize, true, true, true, false);
|
||||
if (thumbnailImage != nil) {
|
||||
dict[@"previewImage"] = thumbnailImage;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -442,7 +442,8 @@
|
|||
if (paintingImage == nil) {
|
||||
paintingImage = adjustments.paintingData.image;
|
||||
}
|
||||
UIImage *thumbnailImage = TGPhotoEditorVideoExtCrop(resultImage, paintingImage, adjustments.cropOrientation, adjustments.cropRotation, adjustments.cropRect, adjustments.cropMirrored, TGScaleToFill(asset.dimensions, CGSizeMake(800, 800)), adjustments.originalSize, true, true, true);
|
||||
UIImage *croppedPaintingImage = TGPhotoEditorPaintingCrop(paintingImage, adjustments.cropOrientation, adjustments.cropRotation, adjustments.cropRect, adjustments.cropMirrored, resultImage.size, adjustments.originalSize, true, true, false);
|
||||
UIImage *thumbnailImage = TGPhotoEditorVideoExtCrop(resultImage, croppedPaintingImage, adjustments.cropOrientation, adjustments.cropRotation, adjustments.cropRect, adjustments.cropMirrored, TGScaleToFill(asset.dimensions, CGSizeMake(800, 800)), adjustments.originalSize, true, true, true, true);
|
||||
if (thumbnailImage != nil) {
|
||||
previewImage = thumbnailImage;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ static void setViewFrame(UIView *view, CGRect frame)
|
|||
{
|
||||
localizationPlaceholderText = TGLocalized(@"MediaPicker.AddCaption");
|
||||
NSString *placeholderText = TGLocalized(@"MediaPicker.AddCaption");
|
||||
UIFont *placeholderFont = TGSystemFontOfSize(16);
|
||||
UIFont *placeholderFont = TGSystemFontOfSize(17);
|
||||
CGSize placeholderSize = [placeholderText sizeWithFont:placeholderFont];
|
||||
placeholderSize.width += 2.0f;
|
||||
placeholderSize.height += 2.0f;
|
||||
|
|
@ -121,7 +121,7 @@ static void setViewFrame(UIView *view, CGRect frame)
|
|||
_placeholderLabel = [[UILabel alloc] init];
|
||||
_placeholderLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;
|
||||
_placeholderLabel.backgroundColor = [UIColor clearColor];
|
||||
_placeholderLabel.font = TGSystemFontOfSize(16);
|
||||
_placeholderLabel.font = TGSystemFontOfSize(17);
|
||||
_placeholderLabel.textColor = UIColorRGB(0x7f7f7f);
|
||||
_placeholderLabel.text = TGLocalized(@"MediaPicker.AddCaption");
|
||||
_placeholderLabel.userInteractionEnabled = true;
|
||||
|
|
@ -130,7 +130,7 @@ static void setViewFrame(UIView *view, CGRect frame)
|
|||
|
||||
_inputFieldOnelineLabel = [[UILabel alloc] init];
|
||||
_inputFieldOnelineLabel.backgroundColor = [UIColor clearColor];
|
||||
_inputFieldOnelineLabel.font = TGSystemFontOfSize(16);
|
||||
_inputFieldOnelineLabel.font = TGSystemFontOfSize(17);
|
||||
_inputFieldOnelineLabel.hidden = true;
|
||||
_inputFieldOnelineLabel.numberOfLines = 1;
|
||||
_inputFieldOnelineLabel.textColor = [UIColor whiteColor];
|
||||
|
|
@ -169,7 +169,7 @@ static void setViewFrame(UIView *view, CGRect frame)
|
|||
_inputField.textColor = [UIColor whiteColor];
|
||||
_inputField.disableFormatting = !_allowEntities;
|
||||
_inputField.placeholderView = _placeholderLabel;
|
||||
_inputField.font = TGSystemFontOfSize(16);
|
||||
_inputField.font = TGSystemFontOfSize(17);
|
||||
_inputField.accentColor = UIColorRGB(0x78b1f9);
|
||||
_inputField.clipsToBounds = true;
|
||||
_inputField.backgroundColor = nil;
|
||||
|
|
@ -188,7 +188,7 @@ static void setViewFrame(UIView *view, CGRect frame)
|
|||
|
||||
_inputField.internalTextView.scrollIndicatorInsets = UIEdgeInsetsMake(-inputFieldInternalEdgeInsets.top, 0, 5 - TGRetinaPixel, 0);
|
||||
|
||||
[_inputField setAttributedText:[TGMediaPickerCaptionInputPanel attributedStringForText:_caption entities:_entities fontSize:16.0f] keepFormatting:true animated:false];
|
||||
[_inputField setAttributedText:[TGMediaPickerCaptionInputPanel attributedStringForText:_caption entities:_entities fontSize:17.0f] keepFormatting:true animated:false];
|
||||
|
||||
[_inputFieldClippingContainer addSubview:_inputField];
|
||||
}
|
||||
|
|
@ -439,7 +439,7 @@ static void setViewFrame(UIView *view, CGRect frame)
|
|||
_fieldBackground.alpha = _placeholderLabel.hidden ? 1.0f : 0.0f;
|
||||
}
|
||||
|
||||
[self.inputField setAttributedText:[TGMediaPickerCaptionInputPanel attributedStringForText:_caption entities:_entities fontSize:16.0f] keepFormatting:true animated:false];
|
||||
[self.inputField setAttributedText:[TGMediaPickerCaptionInputPanel attributedStringForText:_caption entities:_entities fontSize:17.0f] keepFormatting:true animated:false];
|
||||
}
|
||||
|
||||
+ (NSAttributedString *)attributedStringForText:(NSString *)text entities:(NSArray *)entities fontSize:(CGFloat)fontSize {
|
||||
|
|
@ -777,14 +777,14 @@ static void setViewFrame(UIView *view, CGRect frame)
|
|||
if (text == nil)
|
||||
return nil;
|
||||
|
||||
NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithAttributedString:[TGMediaPickerCaptionInputPanel attributedStringForText:text entities:entities fontSize:16.0f]];
|
||||
NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithAttributedString:[TGMediaPickerCaptionInputPanel attributedStringForText:text entities:entities fontSize:17.0f]];
|
||||
|
||||
for (NSUInteger i = 0; i < string.length; i++)
|
||||
{
|
||||
unichar c = [text characterAtIndex:i];
|
||||
if (c == '\t' || c == '\n')
|
||||
{
|
||||
[string insertAttributedString:[[NSAttributedString alloc] initWithString:tokenString attributes:@{NSFontAttributeName:TGSystemFontOfSize(16.0f)}] atIndex:i];
|
||||
[string insertAttributedString:[[NSAttributedString alloc] initWithString:tokenString attributes:@{NSFontAttributeName:TGSystemFontOfSize(17.0f)}] atIndex:i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -167,13 +167,14 @@
|
|||
if (recipientName.length > 0)
|
||||
{
|
||||
_arrowView = [[UIImageView alloc] initWithImage: TGComponentsImageNamed(@"PhotoPickerArrow")];
|
||||
_arrowView.alpha = 0.45f;
|
||||
_arrowView.alpha = 0.6f;
|
||||
[_wrapperView addSubview:_arrowView];
|
||||
|
||||
_recipientLabel = [[UILabel alloc] init];
|
||||
_recipientLabel.alpha = 0.6;
|
||||
_recipientLabel.backgroundColor = [UIColor clearColor];
|
||||
_recipientLabel.font = TGBoldSystemFontOfSize(13.0f);
|
||||
_recipientLabel.textColor = UIColorRGBA(0xffffff, 0.45f);
|
||||
_recipientLabel.textColor = UIColorRGB(0xffffff);
|
||||
_recipientLabel.text = recipientName;
|
||||
_recipientLabel.userInteractionEnabled = false;
|
||||
[_recipientLabel sizeToFit];
|
||||
|
|
@ -510,13 +511,7 @@
|
|||
UIEdgeInsets screenEdges = [self screenEdges];
|
||||
|
||||
__weak TGMediaPickerGalleryInterfaceView *weakSelf = self;
|
||||
|
||||
if ([itemView.headerView isKindOfClass:[TGMediaPickerScrubberHeaderView class]])
|
||||
{
|
||||
TGMediaPickerScrubberHeaderView *headerView = (TGMediaPickerScrubberHeaderView *)itemView.headerView;
|
||||
[headerView.scrubberView setRecipientName:_recipientLabel.text];
|
||||
}
|
||||
|
||||
|
||||
[self _layoutRecipientLabelForOrientation:[self interfaceOrientation] screenEdges:screenEdges hasHeaderView:(itemView.headerView != nil)];
|
||||
|
||||
if (_selectionContext != nil)
|
||||
|
|
@ -1039,8 +1034,8 @@
|
|||
{
|
||||
_checkButton.alpha = alpha;
|
||||
_muteButton.alpha = alpha;
|
||||
_arrowView.alpha = alpha * 0.45f;
|
||||
_recipientLabel.alpha = alpha;
|
||||
_arrowView.alpha = alpha * 0.6f;
|
||||
_recipientLabel.alpha = alpha * 0.6;
|
||||
} completion:^(BOOL finished)
|
||||
{
|
||||
if (finished)
|
||||
|
|
@ -1070,8 +1065,8 @@
|
|||
_muteButton.alpha = alpha;
|
||||
_muteButton.userInteractionEnabled = !hidden;
|
||||
|
||||
_arrowView.alpha = alpha * 0.45f;
|
||||
_recipientLabel.alpha = alpha;
|
||||
_arrowView.alpha = alpha * 0.6f;
|
||||
_recipientLabel.alpha = alpha * 0.6;
|
||||
}
|
||||
|
||||
if (hidden)
|
||||
|
|
@ -1095,7 +1090,7 @@
|
|||
{
|
||||
_checkButton.alpha = alpha;
|
||||
_muteButton.alpha = alpha;
|
||||
_arrowView.alpha = alpha * 0.45f;
|
||||
_arrowView.alpha = alpha * 0.6;
|
||||
_recipientLabel.alpha = alpha;
|
||||
_portraitToolbarView.alpha = alpha;
|
||||
_landscapeToolbarView.alpha = alpha;
|
||||
|
|
@ -1132,7 +1127,7 @@
|
|||
_muteButton.alpha = alpha;
|
||||
_muteButton.userInteractionEnabled = !hidden;
|
||||
|
||||
_arrowView.alpha = alpha * 0.45f;
|
||||
_arrowView.alpha = alpha * 0.6;
|
||||
_recipientLabel.alpha = alpha;
|
||||
|
||||
_portraitToolbarView.alpha = alpha;
|
||||
|
|
@ -1391,6 +1386,8 @@
|
|||
screenEdges.left += _safeAreaInset.left;
|
||||
screenEdges.right -= _safeAreaInset.right;
|
||||
|
||||
CGFloat panelInset = 0.0f;
|
||||
|
||||
switch (orientation)
|
||||
{
|
||||
case UIInterfaceOrientationLandscapeLeft:
|
||||
|
|
@ -1402,13 +1399,10 @@
|
|||
break;
|
||||
|
||||
default:
|
||||
frame = CGRectMake(screenEdges.left + 5, screenEdges.top + 6, _muteButton.frame.size.width, _muteButton.frame.size.height);
|
||||
frame = CGRectMake(screenEdges.left + 5, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 45 - _safeAreaInset.bottom - panelInset - (hasHeaderView ? 64.0 : 0.0), _muteButton.frame.size.width, _muteButton.frame.size.height);
|
||||
break;
|
||||
}
|
||||
|
||||
if (hasHeaderView)
|
||||
frame.origin.y += 64;
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
|
|
@ -1462,9 +1456,6 @@
|
|||
break;
|
||||
}
|
||||
|
||||
if (hasHeaderView)
|
||||
frame.origin.y += 64;
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
|
|
@ -1491,7 +1482,7 @@
|
|||
break;
|
||||
|
||||
default:
|
||||
frame = CGRectMake(screenEdges.right - 46 - _safeAreaInset.right - buttonInset, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 45 - _safeAreaInset.bottom - panelInset, 44, 44);
|
||||
frame = CGRectMake(screenEdges.right - 46 - _safeAreaInset.right - buttonInset, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 45 - _safeAreaInset.bottom - panelInset - (hasHeaderView ? 64.0 : 0.0), 44, 44);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -1526,9 +1517,6 @@
|
|||
|
||||
_arrowView.frame = frame;
|
||||
_recipientLabel.frame = CGRectMake(CGRectGetMaxX(_arrowView.frame) + 6.0f, _arrowView.frame.origin.y - 2.0f, recipientWidth, _recipientLabel.frame.size.height);
|
||||
|
||||
_arrowView.hidden = hasHeaderView;
|
||||
_recipientLabel.hidden = hasHeaderView;
|
||||
}
|
||||
|
||||
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)__unused duration
|
||||
|
|
@ -1670,15 +1658,14 @@
|
|||
{
|
||||
[UIView performWithoutAnimation:^
|
||||
{
|
||||
_photoCounterButton.frame = CGRectMake(screenEdges.right - 56 - _safeAreaInset.right, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 40 - _safeAreaInset.bottom, 64, 38);
|
||||
_photoCounterButton.frame = CGRectMake(screenEdges.right - 56 - _safeAreaInset.right, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 40 - _safeAreaInset.bottom - (hasHeaderView ? 64.0 : 0.0), 64, 38);
|
||||
|
||||
_selectedPhotosView.frame = CGRectMake(screenEdges.left + 4, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - photosViewSize - 54 - _safeAreaInset.bottom, self.frame.size.width - 4 * 2 - _safeAreaInset.right, photosViewSize);
|
||||
_selectedPhotosView.frame = CGRectMake(screenEdges.left + 4, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - photosViewSize - 54 - _safeAreaInset.bottom - (hasHeaderView ? 64.0 : 0.0), self.frame.size.width - 4 * 2 - _safeAreaInset.right, photosViewSize);
|
||||
}];
|
||||
|
||||
_landscapeToolbarView.frame = CGRectMake(_landscapeToolbarView.frame.origin.x, screenEdges.top, TGPhotoEditorToolbarSize, self.frame.size.height);
|
||||
|
||||
CGFloat topInset = _safeAreaInset.top > FLT_EPSILON ? _safeAreaInset.top - 14.0 : 0.0f;
|
||||
_headerWrapperView.frame = CGRectMake(screenEdges.left, screenEdges.top + topInset, self.frame.size.width, 64);
|
||||
_headerWrapperView.frame = CGRectMake(screenEdges.left, _portraitToolbarView.frame.origin.y - 64.0 - [_captionMixin.inputPanel baseHeight], self.frame.size.width, 64.0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@
|
|||
bool _scrubbingPanelPresented;
|
||||
bool _scrubbingPanelLocked;
|
||||
bool _shouldResetScrubber;
|
||||
NSArray *_cachedThumbnails;
|
||||
UIImage *_immediateThumbnail;
|
||||
|
||||
UILabel *_fileInfoLabel;
|
||||
|
||||
|
|
@ -215,13 +217,12 @@
|
|||
_headerView = headerView;
|
||||
_headerView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
|
||||
_scrubberPanelView = [[UIView alloc] initWithFrame:CGRectMake(0, -64, _headerView.frame.size.width, 64)];
|
||||
_scrubberPanelView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, _headerView.frame.size.width, 64)];
|
||||
_scrubberPanelView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
_scrubberPanelView.hidden = true;
|
||||
headerView.panelView = _scrubberPanelView;
|
||||
[_headerView addSubview:_scrubberPanelView];
|
||||
|
||||
UIView *scrubberBackgroundView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, -100.0f, _headerView.frame.size.width, 164.0f)];
|
||||
UIView *scrubberBackgroundView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, _headerView.frame.size.width, 64.0f)];
|
||||
scrubberBackgroundView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
scrubberBackgroundView.backgroundColor = [TGPhotoEditorInterfaceAssets toolbarTransparentBackgroundColor];
|
||||
[_scrubberPanelView addSubview:scrubberBackgroundView];
|
||||
|
|
@ -301,7 +302,6 @@
|
|||
|
||||
_appeared = false;
|
||||
[self setScrubbingPanelApperanceLocked:false];
|
||||
[self setScrubbingPanelHidden:true animated:false];
|
||||
|
||||
[_positionTimer invalidate];
|
||||
_positionTimer = nil;
|
||||
|
|
@ -386,9 +386,14 @@
|
|||
- (void)setItem:(TGMediaPickerGalleryVideoItem *)item synchronously:(bool)synchronously
|
||||
{
|
||||
bool itemChanged = ![item isEqual:self.item];
|
||||
bool itemIdChanged = item.uniqueId != self.item.uniqueId;
|
||||
|
||||
[super setItem:item synchronously:synchronously];
|
||||
|
||||
if (itemIdChanged) {
|
||||
_immediateThumbnail = item.immediateThumbnailImage;
|
||||
}
|
||||
|
||||
if (itemChanged) {
|
||||
[self _playerCleanup];
|
||||
|
||||
|
|
@ -618,17 +623,15 @@
|
|||
|
||||
void (^changeBlock)(void) = ^
|
||||
{
|
||||
_scrubberPanelView.frame = CGRectMake(0.0f, -64.0f - _safeAreaInset.top, _scrubberPanelView.frame.size.width, _scrubberPanelView.frame.size.height);
|
||||
_scrubberPanelView.alpha = 0.0f;
|
||||
};
|
||||
void (^completionBlock)(BOOL) = ^(BOOL finished)
|
||||
{
|
||||
if (finished)
|
||||
_scrubberPanelView.hidden = true;
|
||||
};
|
||||
|
||||
if (animated)
|
||||
{
|
||||
[UIView animateWithDuration:0.3f delay:0.0f options:(7 << 16) animations:changeBlock completion:completionBlock];
|
||||
[UIView animateWithDuration:0.2f delay:0.0f options:UIViewAnimationOptionCurveLinear animations:changeBlock completion:completionBlock];
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -642,18 +645,17 @@
|
|||
return;
|
||||
|
||||
_scrubbingPanelPresented = true;
|
||||
|
||||
_scrubberPanelView.hidden = false;
|
||||
|
||||
[_scrubberPanelView layoutSubviews];
|
||||
[_scrubberView layoutSubviews];
|
||||
|
||||
void (^changeBlock)(void) = ^
|
||||
{
|
||||
_scrubberPanelView.frame = CGRectMake(0.0f, 0.0f, _scrubberPanelView.frame.size.width, _scrubberPanelView.frame.size.height);
|
||||
_scrubberPanelView.alpha = 1.0f;
|
||||
};
|
||||
|
||||
if (animated)
|
||||
[UIView animateWithDuration:0.3f delay:0.0f options:(7 << 16) animations:changeBlock completion:nil];
|
||||
[UIView animateWithDuration:0.2f delay:0.0f options:UIViewAnimationOptionCurveLinear animations:changeBlock completion:nil];
|
||||
else
|
||||
changeBlock();
|
||||
}
|
||||
|
|
@ -708,7 +710,6 @@
|
|||
{
|
||||
[_scrubberView resetThumbnails];
|
||||
|
||||
[self setScrubbingPanelHidden:true animated:false];
|
||||
[_scrubberPanelView setNeedsLayout];
|
||||
[_scrubberPanelView layoutIfNeeded];
|
||||
|
||||
|
|
@ -722,11 +723,14 @@
|
|||
if (_containerView == nil)
|
||||
return;
|
||||
|
||||
_containerView.frame = self.bounds;
|
||||
if (self.bounds.size.width > self.bounds.size.height)
|
||||
_containerView.frame = self.bounds;
|
||||
else
|
||||
_containerView.frame = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height - 44.0);
|
||||
|
||||
[self _layoutPlayerView];
|
||||
|
||||
_videoContentView.frame = (CGRect){CGPointZero, frame.size};
|
||||
_videoContentView.frame = (CGRect){CGPointZero, _containerView.frame.size};
|
||||
|
||||
if (_tooltipContainerView != nil && frame.size.width > frame.size.height)
|
||||
{
|
||||
|
|
@ -753,8 +757,6 @@
|
|||
mirrored = adjustments.cropMirrored;
|
||||
}
|
||||
|
||||
// _scrubberView.maximumLength = adjustments.sendAsGif ? TGVideoEditMaximumGifDuration : 0.0;
|
||||
|
||||
[self _layoutPlayerViewWithCropRect:cropRect videoFrameSize:videoFrameSize orientation:orientation mirrored:mirrored];
|
||||
}
|
||||
|
||||
|
|
@ -1440,40 +1442,8 @@
|
|||
trimEndValue = adjustments.trimEndValue;
|
||||
}
|
||||
}
|
||||
// NSTimeInterval trimDuration = trimEndValue - trimStartValue;
|
||||
|
||||
bool sendAsGif = !adjustments.sendAsGif;
|
||||
// if (sendAsGif && _scrubberView.allowsTrimming)
|
||||
// {
|
||||
// if (trimDuration > TGVideoEditMaximumGifDuration)
|
||||
// {
|
||||
// trimEndValue = trimStartValue + TGVideoEditMaximumGifDuration;
|
||||
//
|
||||
// if (_scrubberView.value > trimEndValue)
|
||||
// {
|
||||
// [self stop];
|
||||
// [_scrubberView setValue:_scrubberView.trimStartValue resetPosition:true];
|
||||
// [self _seekToPosition:_scrubberView.value manual:true];
|
||||
// }
|
||||
//
|
||||
// _scrubberView.trimStartValue = trimStartValue;
|
||||
// _scrubberView.trimEndValue = trimEndValue;
|
||||
// [_scrubberView setTrimApplied:true];
|
||||
// [self updatePlayerRange:trimEndValue];
|
||||
// }
|
||||
// }
|
||||
// else if (_shouldResetScrubber)
|
||||
// {
|
||||
// trimStartValue = 0.0;
|
||||
// trimEndValue = _videoDuration;
|
||||
//
|
||||
// _scrubberView.trimStartValue = trimStartValue;
|
||||
// _scrubberView.trimEndValue = trimEndValue;
|
||||
//
|
||||
// [_scrubberView setTrimApplied:false];
|
||||
// [self updatePlayerRange:trimEndValue];
|
||||
// }
|
||||
|
||||
TGVideoEditAdjustments *updatedAdjustments = [TGVideoEditAdjustments editAdjustmentsWithOriginalSize:_videoDimensions cropRect:cropRect cropOrientation:adjustments.cropOrientation cropRotation:adjustments.cropRotation cropLockedAspectRatio:adjustments.cropLockedAspectRatio cropMirrored:adjustments.cropMirrored trimStartValue:trimStartValue trimEndValue:trimEndValue toolValues:adjustments.toolValues paintingData:adjustments.paintingData sendAsGif:sendAsGif preset:adjustments.preset];
|
||||
[self.item.editingContext setAdjustments:updatedAdjustments forItem:self.item.editableMediaItem];
|
||||
|
||||
|
|
@ -1596,6 +1566,23 @@
|
|||
return timestamps;
|
||||
}
|
||||
|
||||
- (SSignal *)_placeholderThumbnails:(NSArray *)timestamps {
|
||||
NSMutableArray *thumbnails = [[NSMutableArray alloc] init];
|
||||
|
||||
UIImage *image = _immediateThumbnail;
|
||||
if (image == nil)
|
||||
return [SSignal complete];
|
||||
|
||||
UIImage *blurredImage = TGBlurredRectangularImage(image, true, image.size, image.size, NULL, nil);
|
||||
for (__unused NSNumber *value in timestamps) {
|
||||
if (thumbnails.count == 0)
|
||||
[thumbnails addObject:image];
|
||||
else
|
||||
[thumbnails addObject:blurredImage];
|
||||
}
|
||||
return [SSignal single:thumbnails];
|
||||
}
|
||||
|
||||
- (void)videoScrubber:(TGMediaPickerGalleryVideoScrubber *)__unused videoScrubber requestThumbnailImagesForTimestamps:(NSArray *)timestamps size:(CGSize)size isSummaryThumbnails:(bool)isSummaryThumbnails
|
||||
{
|
||||
if (timestamps.count == 0)
|
||||
|
|
@ -1605,17 +1592,42 @@
|
|||
TGMediaEditingContext *editingContext = self.item.editingContext;
|
||||
id<TGMediaEditableItem> editableItem = self.editableMediaItem;
|
||||
|
||||
SSignal *thumbnailsSignal = nil;
|
||||
if ([self.item.asset isKindOfClass:[TGMediaAsset class]] && ![self itemIsLivePhoto])
|
||||
thumbnailsSignal = [TGMediaAssetImageSignals videoThumbnailsForAsset:self.item.asset size:size timestamps:timestamps];
|
||||
else if (avAsset != nil)
|
||||
thumbnailsSignal = [avAsset mapToSignal:^SSignal *(AVAsset *avAsset) {
|
||||
return [TGMediaAssetImageSignals videoThumbnailsForAVAsset:avAsset size:size timestamps:timestamps];
|
||||
}];
|
||||
// SSignal *thumbnailsSignal = nil;
|
||||
// if ([self.item.asset isKindOfClass:[TGMediaAsset class]] && ![self itemIsLivePhoto])
|
||||
// thumbnailsSignal = [TGMediaAssetImageSignals videoThumbnailsForAsset:self.item.asset size:size timestamps:timestamps];
|
||||
// else if (avAsset != nil)
|
||||
// thumbnailsSignal = [avAsset mapToSignal:^SSignal *(AVAsset *avAsset) {
|
||||
// return [TGMediaAssetImageSignals videoThumbnailsForAVAsset:avAsset size:size timestamps:timestamps];
|
||||
// }];
|
||||
|
||||
__strong TGMediaPickerGalleryVideoItemView *weakSelf = self;
|
||||
SSignal *thumbnailsSignal = nil;
|
||||
if (_cachedThumbnails != nil) {
|
||||
thumbnailsSignal = [SSignal single:_cachedThumbnails];
|
||||
} else if ([self.item.asset isKindOfClass:[TGMediaAsset class]] && ![self itemIsLivePhoto]) {
|
||||
thumbnailsSignal = [[self _placeholderThumbnails:timestamps] then:[[TGMediaAssetImageSignals videoThumbnailsForAsset:(TGMediaAsset *)self.item.asset size:size timestamps:timestamps] onNext:^(NSArray *images) {
|
||||
__strong TGMediaPickerGalleryVideoItemView *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
return;
|
||||
|
||||
if (strongSelf->_cachedThumbnails == nil)
|
||||
strongSelf->_cachedThumbnails = images;
|
||||
}]];
|
||||
} else if ([self.item.asset isKindOfClass:[TGCameraCapturedVideo class]]) {
|
||||
thumbnailsSignal = [[((TGCameraCapturedVideo *)self.item.asset).avAsset takeLast] mapToSignal:^SSignal *(AVAsset *avAsset) {
|
||||
return [[self _placeholderThumbnails:timestamps] then:[[TGMediaAssetImageSignals videoThumbnailsForAVAsset:avAsset size:size timestamps:timestamps] onNext:^(NSArray *images) {
|
||||
__strong TGMediaPickerGalleryVideoItemView *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
return;
|
||||
|
||||
if (strongSelf->_cachedThumbnails == nil)
|
||||
strongSelf->_cachedThumbnails = images;
|
||||
}]];
|
||||
}];
|
||||
}
|
||||
|
||||
_requestingThumbnails = true;
|
||||
|
||||
__weak TGMediaPickerGalleryVideoItemView *weakSelf = self;
|
||||
[_thumbnailsDisposable setDisposable:[[[thumbnailsSignal map:^NSArray *(NSArray *images) {
|
||||
id<TGMediaEditAdjustments> adjustments = [editingContext adjustmentsForItem:editableItem];
|
||||
if (adjustments.toolsApplied) {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
- (void)setDotVideoView:(UIView *)dotVideoView;
|
||||
- (void)setDotImage:(UIImage *)dotImage;
|
||||
|
||||
@property (nonatomic, assign) NSTimeInterval minimumLength;
|
||||
@property (nonatomic, assign) NSTimeInterval maximumLength;
|
||||
|
||||
@property (nonatomic, assign) bool disableZoom;
|
||||
|
|
@ -44,8 +45,6 @@
|
|||
|
||||
- (void)setThumbnailImage:(UIImage *)image forTimestamp:(NSTimeInterval)timestamp index:(NSInteger)index isSummaryThubmnail:(bool)isSummaryThumbnail;
|
||||
|
||||
- (void)setRecipientName:(NSString *)recipientName;
|
||||
|
||||
- (CGPoint)scrubberPositionForPosition:(NSTimeInterval)position;
|
||||
|
||||
- (void)_updateScrubberAnimationsAndResetCurrentPosition:(bool)resetCurrentPosition;
|
||||
|
|
|
|||
|
|
@ -81,9 +81,6 @@ typedef enum
|
|||
NSInteger _zoomedPivotTimestampIndex;
|
||||
NSArray *_zoomedTimestamps;
|
||||
NSMutableArray *_zoomedThumbnailViews;
|
||||
|
||||
UIImageView *_arrowView;
|
||||
UILabel *_recipientLabel;
|
||||
}
|
||||
@end
|
||||
|
||||
|
|
@ -95,6 +92,7 @@ typedef enum
|
|||
if (self != nil)
|
||||
{
|
||||
_allowsTrimming = true;
|
||||
_minimumLength = TGVideoScrubberMinimumTrimDuration;
|
||||
|
||||
_currentTimeLabel = [[UILabel alloc] initWithFrame:CGRectMake(8, 4, 100, 15)];
|
||||
_currentTimeLabel.font = TGSystemFontOfSize(12.0f);
|
||||
|
|
@ -237,7 +235,7 @@ typedef enum
|
|||
|
||||
NSTimeInterval duration = trimEndPosition - trimStartPosition;
|
||||
|
||||
if (trimEndPosition - trimStartPosition < TGVideoScrubberMinimumTrimDuration)
|
||||
if (trimEndPosition - trimStartPosition < self.minimumLength)
|
||||
return;
|
||||
|
||||
if (strongSelf.maximumLength > DBL_EPSILON && duration > strongSelf.maximumLength)
|
||||
|
|
@ -300,7 +298,7 @@ typedef enum
|
|||
|
||||
NSTimeInterval duration = trimEndPosition - trimStartPosition;
|
||||
|
||||
if (trimEndPosition - trimStartPosition < TGVideoScrubberMinimumTrimDuration)
|
||||
if (trimEndPosition - trimStartPosition < self.minimumLength)
|
||||
return;
|
||||
|
||||
if (strongSelf.maximumLength > DBL_EPSILON && duration > strongSelf.maximumLength)
|
||||
|
|
@ -416,33 +414,10 @@ typedef enum
|
|||
_tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
|
||||
_tapGestureRecognizer.enabled = false;
|
||||
[_trimView addGestureRecognizer:_tapGestureRecognizer];
|
||||
|
||||
_arrowView = [[UIImageView alloc] initWithImage:TGComponentsImageNamed(@"PhotoPickerArrow")];
|
||||
_arrowView.alpha = 0.45f;
|
||||
_arrowView.hidden = true;
|
||||
[self addSubview:_arrowView];
|
||||
|
||||
_recipientLabel = [[UILabel alloc] init];
|
||||
_recipientLabel.backgroundColor = [UIColor clearColor];
|
||||
_recipientLabel.font = TGBoldSystemFontOfSize(13.0f);
|
||||
_recipientLabel.textColor = UIColorRGBA(0xffffff, 0.45f);
|
||||
_recipientLabel.hidden = true;
|
||||
[self addSubview:_recipientLabel];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setRecipientName:(NSString *)recipientName
|
||||
{
|
||||
_recipientLabel.text = recipientName;
|
||||
_recipientLabel.hidden = recipientName.length == 0;
|
||||
_arrowView.hidden = _recipientLabel.hidden;
|
||||
|
||||
[_recipientLabel sizeToFit];
|
||||
|
||||
[self _layoutRecipientLabel];
|
||||
}
|
||||
|
||||
- (void)setHasDotPicker:(bool)hasDotPicker {
|
||||
_hasDotPicker = hasDotPicker;
|
||||
_tapGestureRecognizer.enabled = hasDotPicker;
|
||||
|
|
@ -1051,11 +1026,9 @@ typedef enum
|
|||
|
||||
- (void)_updateTimeLabels
|
||||
{
|
||||
_currentTimeLabel.text = @"";
|
||||
_currentTimeLabel.text = self.disableTimeDisplay ? @"" : [TGMediaPickerGalleryVideoScrubber _stringFromTotalSeconds:(NSInteger)self.value];
|
||||
|
||||
NSString *text = [NSString stringWithFormat:@"%@ / %@", [TGMediaPickerGalleryVideoScrubber _stringFromTotalSeconds:(NSInteger)self.value], [TGMediaPickerGalleryVideoScrubber _stringFromTotalSeconds:(NSInteger)self.duration]];
|
||||
|
||||
_inverseTimeLabel.text = self.disableTimeDisplay ? @"" : text;
|
||||
_inverseTimeLabel.text = self.disableTimeDisplay ? @"" : [TGMediaPickerGalleryVideoScrubber _stringFromTotalSeconds:(NSInteger)self.duration];
|
||||
}
|
||||
|
||||
#pragma mark - Scrubber Handle
|
||||
|
|
@ -1465,18 +1438,6 @@ typedef enum
|
|||
}
|
||||
}
|
||||
|
||||
- (void)_layoutRecipientLabel
|
||||
{
|
||||
if (self.frame.size.width < FLT_EPSILON)
|
||||
return;
|
||||
|
||||
CGFloat screenWidth = MAX(self.frame.size.width, self.frame.size.height);
|
||||
CGFloat recipientWidth = MIN(_recipientLabel.frame.size.width, screenWidth - 100.0f);
|
||||
|
||||
_arrowView.frame = CGRectMake(14.0f, 6.0f, _arrowView.frame.size.width, _arrowView.frame.size.height);
|
||||
_recipientLabel.frame = CGRectMake(CGRectGetMaxX(_arrowView.frame) + 6.0f, _arrowView.frame.origin.y - 2.0f, recipientWidth, _recipientLabel.frame.size.height);
|
||||
}
|
||||
|
||||
- (void)setFrame:(CGRect)frame
|
||||
{
|
||||
if (isnan(frame.origin.x) || isnan(frame.origin.y) || isnan(frame.size.width) || isnan(frame.size.height))
|
||||
|
|
@ -1500,8 +1461,6 @@ typedef enum
|
|||
_zoomedThumbnailWrapperView.frame = _summaryThumbnailWrapperView.frame;
|
||||
|
||||
[self _updateScrubberAnimationsAndResetCurrentPosition:true];
|
||||
|
||||
[self _layoutRecipientLabel];
|
||||
}
|
||||
|
||||
+ (NSString *)_stringFromTotalSeconds:(NSInteger)totalSeconds
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ const CGFloat TGPhotoCounterButtonMaskFade = 18;
|
|||
{
|
||||
UIGraphicsBeginImageContextWithOptions(CGSizeMake(38.0f, 38.0f), false, 0.0f);
|
||||
CGContextRef context = UIGraphicsGetCurrentContext();
|
||||
CGContextSetFillColorWithColor(context, UIColorRGBA(0x000000, 0.7f).CGColor);
|
||||
CGContextSetFillColorWithColor(context, UIColorRGBA(0x000000, 0.3f).CGColor);
|
||||
|
||||
CGContextFillEllipseInRect(context, CGRectMake(3.5f, 1.0f, 31.0f, 31.0f));
|
||||
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@
|
|||
|
||||
CGSize dimensions = [avAsset tracksWithMediaType:AVMediaTypeVideo].firstObject.naturalSize;
|
||||
TGMediaVideoConversionPreset preset = adjustments.sendAsGif ? TGMediaVideoConversionPresetAnimation : [self presetFromAdjustments:adjustments];
|
||||
if (!CGSizeEqualToSize(dimensions, CGSizeZero) && preset != TGMediaVideoConversionPresetAnimation && preset != TGMediaVideoConversionPresetVideoMessage && preset != TGMediaVideoConversionPresetProfile && preset != TGMediaVideoConversionPresetProfileHigh && preset != TGMediaVideoConversionPresetProfileVeryHigh && preset != TGMediaVideoConversionPresetPassthrough)
|
||||
if (!CGSizeEqualToSize(dimensions, CGSizeZero) && preset != TGMediaVideoConversionPresetAnimation && preset != TGMediaVideoConversionPresetVideoMessage && preset != TGMediaVideoConversionPresetProfile && preset != TGMediaVideoConversionPresetProfileLow && preset != TGMediaVideoConversionPresetProfileHigh && preset != TGMediaVideoConversionPresetProfileVeryHigh && preset != TGMediaVideoConversionPresetPassthrough)
|
||||
{
|
||||
TGMediaVideoConversionPreset bestPreset = [self bestAvailablePresetForDimensions:dimensions];
|
||||
if (preset > bestPreset)
|
||||
|
|
@ -240,7 +240,7 @@
|
|||
return;
|
||||
|
||||
TGMediaVideoConversionPreset preset = TGMediaVideoConversionPresetAnimation;
|
||||
if (adjustments.preset == TGMediaVideoConversionPresetProfile || adjustments.preset == TGMediaVideoConversionPresetProfileHigh || adjustments.preset == TGMediaVideoConversionPresetProfileVeryHigh) {
|
||||
if (adjustments.preset == TGMediaVideoConversionPresetProfile || adjustments.preset != TGMediaVideoConversionPresetProfileLow || adjustments.preset == TGMediaVideoConversionPresetProfileHigh || adjustments.preset == TGMediaVideoConversionPresetProfileVeryHigh) {
|
||||
preset = adjustments.preset;
|
||||
}
|
||||
|
||||
|
|
@ -355,7 +355,7 @@
|
|||
if (TGOrientationIsSideward(adjustments.cropOrientation, NULL))
|
||||
outputDimensions = CGSizeMake(outputDimensions.height, outputDimensions.width);
|
||||
|
||||
if ((preset == TGMediaVideoConversionPresetProfile || preset == TGMediaVideoConversionPresetProfileHigh || preset == TGMediaVideoConversionPresetProfileVeryHigh) && MIN(outputDimensions.width, outputDimensions.height) < 160.0) {
|
||||
if ((preset == TGMediaVideoConversionPresetProfile || preset == TGMediaVideoConversionPresetProfileLow || preset == TGMediaVideoConversionPresetProfileHigh || preset == TGMediaVideoConversionPresetProfileVeryHigh) && MIN(outputDimensions.width, outputDimensions.height) < 160.0) {
|
||||
outputDimensions = CGSizeMake(160.0, 160.0);
|
||||
}
|
||||
|
||||
|
|
@ -1255,6 +1255,9 @@ static CGFloat progressOfSampleBufferInTimeRange(CMSampleBufferRef sampleBuffer,
|
|||
case TGMediaVideoConversionPresetVideoMessage:
|
||||
return (CGSize){ 240.0f, 240.0f };
|
||||
|
||||
case TGMediaVideoConversionPresetProfileLow:
|
||||
return (CGSize){ 720.0f, 720.0f };
|
||||
|
||||
case TGMediaVideoConversionPresetProfile:
|
||||
case TGMediaVideoConversionPresetProfileHigh:
|
||||
case TGMediaVideoConversionPresetProfileVeryHigh:
|
||||
|
|
@ -1267,7 +1270,7 @@ static CGFloat progressOfSampleBufferInTimeRange(CMSampleBufferRef sampleBuffer,
|
|||
|
||||
+ (bool)keepAudioForPreset:(TGMediaVideoConversionPreset)preset
|
||||
{
|
||||
return preset != TGMediaVideoConversionPresetAnimation && preset != TGMediaVideoConversionPresetProfile && preset != TGMediaVideoConversionPresetProfileHigh && preset != TGMediaVideoConversionPresetProfileVeryHigh;
|
||||
return preset != TGMediaVideoConversionPresetAnimation && preset != TGMediaVideoConversionPresetProfile && preset != TGMediaVideoConversionPresetProfileLow && preset != TGMediaVideoConversionPresetProfileHigh && preset != TGMediaVideoConversionPresetProfileVeryHigh;
|
||||
}
|
||||
|
||||
+ (NSDictionary *)audioSettingsForPreset:(TGMediaVideoConversionPreset)preset
|
||||
|
|
@ -1346,11 +1349,14 @@ static CGFloat progressOfSampleBufferInTimeRange(CMSampleBufferRef sampleBuffer,
|
|||
case TGMediaVideoConversionPresetProfile:
|
||||
return 1500;
|
||||
|
||||
case TGMediaVideoConversionPresetProfileLow:
|
||||
return 1100;
|
||||
|
||||
case TGMediaVideoConversionPresetProfileHigh:
|
||||
return 2000;
|
||||
|
||||
case TGMediaVideoConversionPresetProfileVeryHigh:
|
||||
return 2500;
|
||||
return 2400;
|
||||
|
||||
default:
|
||||
return 900;
|
||||
|
|
@ -1381,6 +1387,7 @@ static CGFloat progressOfSampleBufferInTimeRange(CMSampleBufferRef sampleBuffer,
|
|||
|
||||
case TGMediaVideoConversionPresetAnimation:
|
||||
case TGMediaVideoConversionPresetProfile:
|
||||
case TGMediaVideoConversionPresetProfileLow:
|
||||
case TGMediaVideoConversionPresetProfileHigh:
|
||||
case TGMediaVideoConversionPresetProfileVeryHigh:
|
||||
return 0;
|
||||
|
|
@ -1411,6 +1418,7 @@ static CGFloat progressOfSampleBufferInTimeRange(CMSampleBufferRef sampleBuffer,
|
|||
|
||||
case TGMediaVideoConversionPresetAnimation:
|
||||
case TGMediaVideoConversionPresetProfile:
|
||||
case TGMediaVideoConversionPresetProfileLow:
|
||||
case TGMediaVideoConversionPresetProfileHigh:
|
||||
case TGMediaVideoConversionPresetProfileVeryHigh:
|
||||
return 0;
|
||||
|
|
|
|||
|
|
@ -146,10 +146,19 @@ const CGFloat TGPhotoAvatarCropViewCurtainMargin = 200;
|
|||
|
||||
UITapGestureRecognizer *tapRecognier = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
|
||||
[_wrapperView addGestureRecognizer:tapRecognier];
|
||||
|
||||
_clipView = [[UIView alloc] init];
|
||||
_clipView.clipsToBounds = true;
|
||||
_clipView.userInteractionEnabled = false;
|
||||
[self addSubview:_clipView];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)attachEntitiesView {
|
||||
[_entitiesWrapperView addSubview:_fullEntitiesView];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
_scrollView.delegate = nil;
|
||||
|
|
@ -644,6 +653,8 @@ const CGFloat TGPhotoAvatarCropViewCurtainMargin = 200;
|
|||
{
|
||||
[self _layoutOverlayViews];
|
||||
|
||||
_clipView.frame = self.bounds;
|
||||
|
||||
_flashView.frame = self.bounds;
|
||||
|
||||
if (_scrollView.superview == nil)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
#import "TGMediaPickerGalleryVideoScrubber.h"
|
||||
#import "TGModernGalleryVideoView.h"
|
||||
#import "TGPhotoEntitiesContainerView.h"
|
||||
|
||||
#import "TGPhotoPaintController.h"
|
||||
|
||||
|
|
@ -257,6 +258,58 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
|
|||
[self.view insertSubview:_transitionView belowSubview:_wrapperView];
|
||||
}
|
||||
|
||||
- (void)animateTransitionIn {
|
||||
if (self.initialAppearance) {
|
||||
[super animateTransitionIn];
|
||||
return;
|
||||
} else {
|
||||
_animateScale = true;
|
||||
|
||||
[self transitEntities:_previewView];
|
||||
|
||||
[super animateTransitionIn];
|
||||
}
|
||||
}
|
||||
|
||||
+ (CGRect)fittedCropRect:(CGRect)cropRect originalSize:(CGSize)originalSize fitSize:(CGSize)fitSize {
|
||||
CGSize fittedOriginalSize = TGScaleToSize(originalSize, fitSize);
|
||||
CGFloat scale = fittedOriginalSize.width / originalSize.width;
|
||||
|
||||
CGSize size = fittedOriginalSize;
|
||||
|
||||
return CGRectMake(-cropRect.origin.x * scale, -cropRect.origin.y * scale, size.width, size.height);
|
||||
}
|
||||
|
||||
- (void)transitEntities:(UIView *)parentView {
|
||||
UIView *containerView = [[UIView alloc] init];
|
||||
[parentView addSubview:containerView];
|
||||
|
||||
containerView.frame = CGRectMake(0.0, 0.0, _fullEntitiesView.frame.size.width, _fullEntitiesView.frame.size.height);
|
||||
[containerView addSubview:_fullEntitiesView];
|
||||
|
||||
CGFloat paintingScale = _fullEntitiesView.frame.size.width / _photoEditor.originalSize.width;
|
||||
_fullEntitiesView.frame = CGRectMake(-_photoEditor.cropRect.origin.x * paintingScale, -_photoEditor.cropRect.origin.y * paintingScale, _fullEntitiesView.frame.size.width, _fullEntitiesView.frame.size.height);
|
||||
|
||||
CGFloat cropScale = 1.0;
|
||||
if (_photoEditor.originalSize.width > _photoEditor.originalSize.height) {
|
||||
cropScale = _photoEditor.originalSize.height / _photoEditor.cropRect.size.height;
|
||||
} else {
|
||||
cropScale = _photoEditor.originalSize.width / _photoEditor.cropRect.size.width;
|
||||
}
|
||||
|
||||
UIImageOrientation imageOrientation = _photoEditor.cropOrientation;
|
||||
if ([parentView isKindOfClass:[TGPhotoEditorPreviewView class]])
|
||||
imageOrientation = UIImageOrientationUp;
|
||||
|
||||
CGAffineTransform rotationTransform = CGAffineTransformMakeRotation(TGRotationForOrientation(imageOrientation));
|
||||
if ([parentView isKindOfClass:[TGPhotoEditorPreviewView class]] && _photoEditor.cropMirrored) {
|
||||
rotationTransform = CGAffineTransformMakeScale(-1.0, 1.0);
|
||||
}
|
||||
CGFloat scale = parentView.frame.size.width / _fullEntitiesView.frame.size.width;
|
||||
containerView.transform = CGAffineTransformScale(rotationTransform, scale * cropScale, scale * cropScale);
|
||||
containerView.frame = CGRectMake(0.0, 0.0, parentView.frame.size.width, parentView.frame.size.height);
|
||||
}
|
||||
|
||||
- (void)transitionIn
|
||||
{
|
||||
if (_portraitToolsWrapperView.frame.size.height < FLT_EPSILON) {
|
||||
|
|
@ -373,13 +426,22 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
|
|||
|
||||
[_cropView closeCurtains];
|
||||
|
||||
[self transitEntities:_cropView.clipView];
|
||||
|
||||
CGAffineTransform initialTransform = _previewView.transform;
|
||||
[UIView animateWithDuration:0.3f delay:0.0f options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionLayoutSubviews animations:^
|
||||
{
|
||||
_previewView.frame = targetFrame;
|
||||
CGFloat scale = targetFrame.size.width / _previewView.frame.size.width;
|
||||
_previewView.center = CGPointMake(CGRectGetMidX(targetFrame), CGRectGetMidY(targetFrame));
|
||||
_previewView.transform = CGAffineTransformScale(initialTransform, scale, scale);
|
||||
|
||||
_cropView.center = CGPointMake(CGRectGetMidX(targetCropViewFrame), CGRectGetMidY(targetCropViewFrame));
|
||||
_cropView.transform = CGAffineTransformMakeScale(targetCropViewScale, targetCropViewScale);
|
||||
} completion:^(__unused BOOL finished)
|
||||
{
|
||||
{
|
||||
_fullEntitiesView.frame = CGRectMake(0, 0, _fullEntitiesView.frame.size.width, _fullEntitiesView.frame.size.height);
|
||||
_previewView.transform = initialTransform;
|
||||
_previewView.frame = targetFrame;
|
||||
[_cropView removeFromSuperview];
|
||||
_previewView.alpha = 1.0;
|
||||
if (self.finishedTransitionOut != nil)
|
||||
|
|
@ -504,8 +566,13 @@ const CGFloat TGPhotoAvatarPreviewLandscapePanelSize = TGPhotoAvatarPreviewPanel
|
|||
{
|
||||
_appeared = true;
|
||||
|
||||
if (!self.initialAppearance) {
|
||||
[_fullEntitiesView.superview removeFromSuperview];
|
||||
_fullEntitiesView.frame = CGRectMake(0, 0, _fullEntitiesView.frame.size.width, _fullEntitiesView.frame.size.height);
|
||||
[_cropView attachEntitiesView];
|
||||
}
|
||||
|
||||
if ([transitionView isKindOfClass:[TGPhotoEditorPreviewView class]]) {
|
||||
|
||||
} else {
|
||||
[transitionView removeFromSuperview];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@
|
|||
UIImage *_thumbnailImage;
|
||||
|
||||
CMTime _chaseTime;
|
||||
bool _chaseStart;
|
||||
bool _chasingTime;
|
||||
bool _isPlaying;
|
||||
AVPlayerItem *_playerItem;
|
||||
|
|
@ -297,8 +298,7 @@
|
|||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
TGPhotoEditorBackButton backButton = TGPhotoEditorBackButtonCancel;
|
||||
TGPhotoEditorDoneButton doneButton = TGPhotoEditorDoneButtonCheck;
|
||||
_portraitToolbarView = [[TGPhotoToolbarView alloc] initWithBackButton:backButton doneButton:doneButton solidBackground:true];
|
||||
|
|
@ -368,6 +368,7 @@
|
|||
|
||||
if ([self presentedForAvatarCreation] && _item.isVideo) {
|
||||
_scrubberView = [[TGMediaPickerGalleryVideoScrubber alloc] initWithFrame:CGRectMake(0.0f, 0.0, _portraitToolbarView.frame.size.width, 68.0f)];
|
||||
_scrubberView.minimumLength = 3.0;
|
||||
_scrubberView.layer.allowsGroupOpacity = true;
|
||||
_scrubberView.hasDotPicker = true;
|
||||
_scrubberView.dataSource = self;
|
||||
|
|
@ -481,6 +482,8 @@
|
|||
if ([adjustments isKindOfClass:[TGMediaVideoEditAdjustments class]])
|
||||
position = adjustments.trimStartValue;
|
||||
|
||||
PGPhotoEditor *photoEditor = _photoEditor;
|
||||
|
||||
CGSize screenSize = TGNativeScreenSize();
|
||||
SSignal *signal = nil;
|
||||
if ([_photoEditor hasDefaultCropping] && (NSInteger)screenSize.width == 320)
|
||||
|
|
@ -504,20 +507,18 @@
|
|||
if (avatar) {
|
||||
return image;
|
||||
} else {
|
||||
return TGPhotoEditorCrop(image, nil, _photoEditor.cropOrientation, _photoEditor.cropRotation, _photoEditor.cropRect, _photoEditor.cropMirrored, TGPhotoEditorScreenImageMaxSize(), _photoEditor.originalSize, true);
|
||||
return TGPhotoEditorCrop(image, nil, photoEditor.cropOrientation, photoEditor.cropRotation, photoEditor.cropRect, photoEditor.cropMirrored, TGPhotoEditorScreenImageMaxSize(), photoEditor.originalSize, true);
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
__weak TGPhotoEditorController *weakSelf = self;
|
||||
PGPhotoEditor *photoEditor = _photoEditor;
|
||||
|
||||
[signal startWithNext:^(id next)
|
||||
{
|
||||
__strong TGPhotoEditorController *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
return;
|
||||
if (strongSelf == nil)
|
||||
return;
|
||||
|
||||
if (strongSelf->_dismissed)
|
||||
return;
|
||||
|
|
@ -566,7 +567,6 @@
|
|||
|
||||
if (strongSelf->_hadProgress && !progressVisible) {
|
||||
[strongSelf->_progressView setPlay];
|
||||
|
||||
[strongSelf->_scrubberView reloadThumbnails];
|
||||
}
|
||||
});
|
||||
|
|
@ -622,7 +622,10 @@
|
|||
|
||||
- (NSTimeInterval)trimEndValue {
|
||||
if (_scrubberView != nil) {
|
||||
return _scrubberView.trimEndValue;
|
||||
if (_scrubberView.trimEndValue > 0.0)
|
||||
return _scrubberView.trimEndValue;
|
||||
else
|
||||
return MIN(9.9, _scrubberView.duration);
|
||||
} else {
|
||||
return _photoEditor.trimEndValue;
|
||||
}
|
||||
|
|
@ -669,6 +672,9 @@
|
|||
if (strongSelf != nil && !strongSelf->_dismissed) {
|
||||
[strongSelf->_player seekToTime:startTime];
|
||||
[strongSelf->_scrubberView setValue:strongSelf.trimStartValue resetPosition:true];
|
||||
|
||||
[strongSelf->_fullEntitiesView seekTo:0.0];
|
||||
[strongSelf->_fullEntitiesView play];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
|
@ -698,6 +704,11 @@
|
|||
[_player addObserver:self forKeyPath:@"rate" options:NSKeyValueObservingOptionNew context:nil];
|
||||
_registeredKeypathObserver = true;
|
||||
}
|
||||
|
||||
[_fullEntitiesView seekTo:0.0];
|
||||
[_fullEntitiesView play];
|
||||
} else {
|
||||
[_fullEntitiesView play];
|
||||
}
|
||||
|
||||
_isPlaying = true;
|
||||
|
|
@ -721,6 +732,8 @@
|
|||
}
|
||||
|
||||
[_scrubberView setIsPlaying:false];
|
||||
} else {
|
||||
[_fullEntitiesView pause];
|
||||
}
|
||||
|
||||
_isPlaying = false;
|
||||
|
|
@ -747,6 +760,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
- (NSTimeInterval)currentTime {
|
||||
return CMTimeGetSeconds(_player.currentItem.currentTime) - [self trimStartValue];
|
||||
}
|
||||
|
||||
- (void)setMinimalVideoDuration:(NSTimeInterval)duration {
|
||||
_scrubberView.minimumLength = duration;
|
||||
}
|
||||
|
||||
- (void)seekVideo:(NSTimeInterval)position {
|
||||
CMTime targetTime = CMTimeMakeWithSeconds(position, NSEC_PER_SEC);
|
||||
|
||||
|
|
@ -765,6 +786,11 @@
|
|||
CMTime currentChasingTime = _chaseTime;
|
||||
|
||||
[_player.currentItem seekToTime:currentChasingTime toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:^(BOOL finished) {
|
||||
if (!_chaseStart) {
|
||||
TGDispatchOnMainThread(^{
|
||||
[_fullEntitiesView seekTo:CMTimeGetSeconds(currentChasingTime) - _scrubberView.trimStartValue];
|
||||
});
|
||||
}
|
||||
if (CMTIME_COMPARE_INLINE(currentChasingTime, ==, _chaseTime)) {
|
||||
_chasingTime = false;
|
||||
_chaseTime = kCMTimeInvalid;
|
||||
|
|
@ -965,6 +991,7 @@
|
|||
}];
|
||||
};
|
||||
|
||||
SQueue *queue = _queue;
|
||||
SSignal *(^imageRenderSignal)(UIImage *) = ^(UIImage *image)
|
||||
{
|
||||
return [[SSignal alloc] initWithGenerator:^id<SDisposable>(SSubscriber *subscriber)
|
||||
|
|
@ -972,7 +999,7 @@
|
|||
[photoEditor setImage:image forCropRect:photoEditor.cropRect cropRotation:photoEditor.cropRotation cropOrientation:photoEditor.cropOrientation cropMirrored:photoEditor.cropMirrored fullSize:true];
|
||||
[photoEditor createResultImageWithCompletion:^(UIImage *result)
|
||||
{
|
||||
[_queue dispatch:^{
|
||||
[queue dispatch:^{
|
||||
UIImage *final = result;
|
||||
if (hasPainting)
|
||||
{
|
||||
|
|
@ -995,7 +1022,7 @@
|
|||
}] mapToSignal:^SSignal *(UIImage *image)
|
||||
{
|
||||
if (hasImageAdjustments)
|
||||
return [[[SSignal complete] delay:0.3 onQueue:_queue] then:imageRenderSignal(image)];
|
||||
return [[[SSignal complete] delay:0.3 onQueue:queue] then:imageRenderSignal(image)];
|
||||
else
|
||||
return [SSignal single:image];
|
||||
}];
|
||||
|
|
@ -1171,6 +1198,7 @@
|
|||
UIView *snapshotView = nil;
|
||||
|
||||
TGPhotoEditorTabController *currentController = _currentTabController;
|
||||
TGPhotoEditorTab switchingFromTab = TGPhotoEditorNoneTab;
|
||||
if (currentController != nil)
|
||||
{
|
||||
if (![currentController isDismissAllowed])
|
||||
|
|
@ -1178,13 +1206,18 @@
|
|||
|
||||
[self savePaintingData];
|
||||
|
||||
bool resetTransform = false;
|
||||
if ([self presentedForAvatarCreation] && tab == TGPhotoEditorCropTab && [currentController isKindOfClass:[TGPhotoPaintController class]]) {
|
||||
resetTransform = true;
|
||||
}
|
||||
|
||||
currentController.switchingToTab = tab;
|
||||
[currentController transitionOutSwitching:true completion:^
|
||||
{
|
||||
[currentController removeFromParentViewController];
|
||||
[currentController.view removeFromSuperview];
|
||||
|
||||
if ([self presentedForAvatarCreation] && tab == TGPhotoEditorCropTab) {
|
||||
if (resetTransform) {
|
||||
_previewView.transform = CGAffineTransformIdentity;
|
||||
}
|
||||
}];
|
||||
|
|
@ -1200,6 +1233,9 @@
|
|||
{
|
||||
_backgroundView.alpha = 0.0f;
|
||||
} completion:nil];
|
||||
switchingFromTab = TGPhotoEditorCropTab;
|
||||
} else if ([currentController isKindOfClass:[TGPhotoToolsController class]]) {
|
||||
switchingFromTab = TGPhotoEditorToolsTab;
|
||||
}
|
||||
|
||||
isInitialAppearance = false;
|
||||
|
|
@ -1281,7 +1317,7 @@
|
|||
cropController.toolbarLandscapeSize = TGPhotoEditorToolbarSize;
|
||||
cropController.controlVideoPlayback = ^(bool play) {
|
||||
__strong TGPhotoEditorController *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
if (strongSelf == nil || strongSelf->_progressVisible)
|
||||
return;
|
||||
if (play) {
|
||||
[strongSelf startVideoPlayback:false];
|
||||
|
|
@ -1297,7 +1333,7 @@
|
|||
};
|
||||
cropController.togglePlayback = ^{
|
||||
__strong TGPhotoEditorController *strongSelf = weakSelf;
|
||||
if (strongSelf == nil || !strongSelf->_item.isVideo)
|
||||
if (strongSelf == nil || !strongSelf->_item.isVideo || strongSelf->_progressVisible)
|
||||
return;
|
||||
|
||||
if (strongSelf->_isPlaying) {
|
||||
|
|
@ -1613,6 +1649,7 @@
|
|||
_currentTabController = controller;
|
||||
_currentTabController.item = _item;
|
||||
_currentTabController.intent = _intent;
|
||||
_currentTabController.switchingFromTab = switchingFromTab;
|
||||
_currentTabController.initialAppearance = isInitialAppearance;
|
||||
|
||||
if (![_currentTabController isKindOfClass:[TGPhotoPaintController class]])
|
||||
|
|
@ -1915,7 +1952,7 @@
|
|||
{
|
||||
videoStartValue = _dotPosition;
|
||||
trimStartValue = self.trimStartValue;
|
||||
trimEndValue = self.trimEndValue;
|
||||
trimEndValue = MIN(self.trimStartValue + 9.9, self.trimEndValue);
|
||||
}
|
||||
|
||||
[self stopVideoPlayback:true];
|
||||
|
|
@ -1945,7 +1982,7 @@
|
|||
generator.requestedTimeToleranceAfter = kCMTimeZero;
|
||||
generator.requestedTimeToleranceBefore = kCMTimeZero;
|
||||
|
||||
CGImageRef imageRef = [generator copyCGImageAtTime:CMTimeMakeWithSeconds(videoStartValue, NSEC_PER_SEC) actualTime:nil error:NULL];
|
||||
CGImageRef imageRef = [generator copyCGImageAtTime:CMTimeMakeWithSeconds(MIN(videoStartValue, CMTimeGetSeconds(asset.duration) - 0.05), NSEC_PER_SEC) actualTime:nil error:NULL];
|
||||
UIImage *image = [UIImage imageWithCGImage:imageRef];
|
||||
CGImageRelease(imageRef);
|
||||
|
||||
|
|
@ -1979,10 +2016,16 @@
|
|||
|
||||
NSTimeInterval duration = trimEndValue - trimStartValue;
|
||||
TGMediaVideoConversionPreset preset;
|
||||
if (duration <= 2.5) {
|
||||
preset = TGMediaVideoConversionPresetProfileVeryHigh;
|
||||
} else if (duration <= 5.0) {
|
||||
preset = TGMediaVideoConversionPresetProfileHigh;
|
||||
if (duration > 0.0) {
|
||||
if (duration <= 2.0) {
|
||||
preset = TGMediaVideoConversionPresetProfileVeryHigh;
|
||||
} else if (duration <= 5.0) {
|
||||
preset = TGMediaVideoConversionPresetProfileHigh;
|
||||
} else if (duration <= 8.0) {
|
||||
preset = TGMediaVideoConversionPresetProfile;
|
||||
} else {
|
||||
preset = TGMediaVideoConversionPresetProfileLow;
|
||||
}
|
||||
} else {
|
||||
preset = TGMediaVideoConversionPresetProfile;
|
||||
}
|
||||
|
|
@ -2725,7 +2768,7 @@
|
|||
});
|
||||
}
|
||||
|
||||
- (void)videoScrubber:(TGMediaPickerGalleryVideoScrubber *)__unused videoScrubber valueDidChange:(NSTimeInterval)position
|
||||
- (void)videoScrubber:(TGMediaPickerGalleryVideoScrubber *)videoScrubber valueDidChange:(NSTimeInterval)position
|
||||
{
|
||||
[self seekVideo:position];
|
||||
}
|
||||
|
|
@ -2762,6 +2805,8 @@
|
|||
[self startVideoPlayback:true];
|
||||
|
||||
[self setPlayButtonHidden:true animated:false];
|
||||
|
||||
_chaseStart = false;
|
||||
}
|
||||
|
||||
- (void)videoScrubber:(TGMediaPickerGalleryVideoScrubber *)videoScrubber editingStartValueDidChange:(NSTimeInterval)startValue
|
||||
|
|
@ -2770,6 +2815,12 @@
|
|||
_resetDotPosition = true;
|
||||
[self resetDotImage];
|
||||
}
|
||||
|
||||
if (!_chaseStart) {
|
||||
_chaseStart = true;
|
||||
[_fullEntitiesView resetToStart];
|
||||
}
|
||||
|
||||
[self seekVideo:startValue];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@
|
|||
CGRect rect = CGRectMake(0, 0, 39.0f, 39.0f);
|
||||
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0);
|
||||
CGContextRef context = UIGraphicsGetCurrentContext();
|
||||
CGContextSetFillColorWithColor(context, UIColorRGBA(0x000000, 0.6f).CGColor);
|
||||
CGContextSetFillColorWithColor(context, UIColorRGBA(0x000000, 0.3f).CGColor);
|
||||
CGContextFillEllipseInRect(context, CGRectInset(rect, 3, 3));
|
||||
|
||||
muteBackground = UIGraphicsGetImageFromCurrentImageContext();
|
||||
|
|
|
|||
|
|
@ -169,22 +169,34 @@ const CGFloat TGPhotoEditorToolbarSize = 49.0f;
|
|||
|
||||
_transitionInProgress = true;
|
||||
|
||||
CGAffineTransform initialTransform = _transitionView.transform;
|
||||
[UIView animateWithDuration:0.3f delay:0.0f options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionLayoutSubviews animations:^
|
||||
{
|
||||
_transitionView.frame = _transitionTargetFrame;
|
||||
if (_animateScale) {
|
||||
CGFloat scale = _transitionTargetFrame.size.width / _transitionView.frame.size.width;
|
||||
_transitionView.center = CGPointMake(CGRectGetMidX(_transitionTargetFrame), CGRectGetMidY(_transitionTargetFrame));
|
||||
_transitionView.transform = CGAffineTransformScale(initialTransform, scale, scale);
|
||||
} else {
|
||||
_transitionView.frame = _transitionTargetFrame;
|
||||
}
|
||||
} completion:^(BOOL finished) {
|
||||
_transitionInProgress = false;
|
||||
|
||||
UIView *transitionView = _transitionView;
|
||||
_transitionView = nil;
|
||||
|
||||
if (self.finishedTransitionIn != nil)
|
||||
{
|
||||
self.finishedTransitionIn();
|
||||
self.finishedTransitionIn = nil;
|
||||
}
|
||||
|
||||
[self _finishedTransitionInWithView:transitionView];
|
||||
UIView *transitionView = _transitionView;
|
||||
_transitionView = nil;
|
||||
|
||||
if (_animateScale) {
|
||||
_transitionView.transform = initialTransform;
|
||||
_transitionView.frame = _transitionTargetFrame;
|
||||
}
|
||||
|
||||
if (self.finishedTransitionIn != nil)
|
||||
{
|
||||
self.finishedTransitionIn();
|
||||
self.finishedTransitionIn = nil;
|
||||
}
|
||||
|
||||
[self _finishedTransitionInWithView:transitionView];
|
||||
}];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -218,10 +218,10 @@ UIImage *TGPhotoEditorCrop(UIImage *inputImage, UIImage *paintingImage, UIImageO
|
|||
}
|
||||
|
||||
UIImage *TGPhotoEditorVideoCrop(UIImage *inputImage, UIImage *paintingImage, UIImageOrientation orientation, CGFloat rotation, CGRect rect, bool mirrored, CGSize maxSize, CGSize originalSize, bool shouldResize, bool useImageSize) {
|
||||
return TGPhotoEditorVideoExtCrop(inputImage, paintingImage, orientation, rotation, rect, mirrored, maxSize, originalSize, shouldResize, useImageSize, false);
|
||||
return TGPhotoEditorVideoExtCrop(inputImage, paintingImage, orientation, rotation, rect, mirrored, maxSize, originalSize, shouldResize, useImageSize, false, false);
|
||||
}
|
||||
|
||||
UIImage *TGPhotoEditorVideoExtCrop(UIImage *inputImage, UIImage *paintingImage, UIImageOrientation orientation, CGFloat rotation, CGRect rect, bool mirrored, CGSize maxSize, CGSize originalSize, bool shouldResize, bool useImageSize, bool skipImageTransform)
|
||||
UIImage *TGPhotoEditorVideoExtCrop(UIImage *inputImage, UIImage *paintingImage, UIImageOrientation orientation, CGFloat rotation, CGRect rect, bool mirrored, CGSize maxSize, CGSize originalSize, bool shouldResize, bool useImageSize, bool skipImageTransform, bool fillPainting)
|
||||
{
|
||||
if (iosMajorVersion() < 7)
|
||||
return TGPhotoEditorLegacyCrop(inputImage, paintingImage, orientation, rotation, rect, mirrored, maxSize, shouldResize);
|
||||
|
|
@ -251,6 +251,9 @@ UIImage *TGPhotoEditorVideoExtCrop(UIImage *inputImage, UIImage *paintingImage,
|
|||
|
||||
UIGraphicsBeginImageContextWithOptions(CGSizeMake(outputImageSize.width, outputImageSize.height), true, 1.0f);
|
||||
CGContextRef context = UIGraphicsGetCurrentContext();
|
||||
|
||||
CGContextSaveGState(context);
|
||||
|
||||
CGContextSetFillColorWithColor(context, [UIColor blackColor].CGColor);
|
||||
CGContextFillRect(context, CGRectMake(0, 0, outputImageSize.width, outputImageSize.height));
|
||||
CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
|
||||
|
|
@ -297,6 +300,97 @@ UIImage *TGPhotoEditorVideoExtCrop(UIImage *inputImage, UIImage *paintingImage,
|
|||
[image drawAtPoint:CGPointMake(-image.size.width / 2, -image.size.height / 2)];
|
||||
}
|
||||
|
||||
if (paintingImage != nil)
|
||||
{
|
||||
if (fillPainting) {
|
||||
CGContextRestoreGState(context);
|
||||
[paintingImage drawInRect:CGRectMake(0.0, 0.0, outputImageSize.width, outputImageSize.height)];
|
||||
} else {
|
||||
if (mirrored)
|
||||
CGContextScaleCTM(context, -1.0f, 1.0f);
|
||||
|
||||
[paintingImage drawInRect:CGRectMake(-imageSize.width / 2, -imageSize.height / 2, imageSize.width, imageSize.height)];
|
||||
}
|
||||
}
|
||||
|
||||
UIImage *croppedImage = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
|
||||
return croppedImage;
|
||||
}
|
||||
|
||||
UIImage *TGPhotoEditorPaintingCrop(UIImage *paintingImage, UIImageOrientation orientation, CGFloat rotation, CGRect rect, bool mirrored, CGSize maxSize, CGSize originalSize, bool shouldResize, bool useImageSize, bool skipImageTransform)
|
||||
{
|
||||
CGSize fittedOriginalSize = originalSize;
|
||||
if (useImageSize)
|
||||
{
|
||||
CGFloat ratio = paintingImage.size.width / originalSize.width;
|
||||
if (skipImageTransform) {
|
||||
|
||||
}
|
||||
rect.origin.x = rect.origin.x * ratio;
|
||||
rect.origin.y = rect.origin.y * ratio;
|
||||
rect.size.width = rect.size.width * ratio;
|
||||
rect.size.height = rect.size.height * ratio;
|
||||
|
||||
fittedOriginalSize = CGSizeMake(originalSize.width * ratio, originalSize.height * ratio);
|
||||
}
|
||||
|
||||
CGSize fittedImageSize = shouldResize ? TGFitSize(rect.size, maxSize) : rect.size;
|
||||
|
||||
CGSize outputImageSize = fittedImageSize;
|
||||
outputImageSize.width = CGFloor(outputImageSize.width);
|
||||
outputImageSize.height = CGFloor(outputImageSize.height);
|
||||
if (TGOrientationIsSideward(orientation, NULL))
|
||||
outputImageSize = CGSizeMake(outputImageSize.height, outputImageSize.width);
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(CGSizeMake(outputImageSize.width, outputImageSize.height), false, 1.0f);
|
||||
CGContextRef context = UIGraphicsGetCurrentContext();
|
||||
|
||||
UIImage *image = nil;
|
||||
CGSize imageSize = paintingImage.size;
|
||||
if (shouldResize)
|
||||
{
|
||||
CGSize referenceSize = useImageSize ? paintingImage.size : originalSize;
|
||||
CGSize resizedSize = CGSizeMake(referenceSize.width * fittedImageSize.width / rect.size.width, referenceSize.height * fittedImageSize.height / rect.size.height);
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(resizedSize, false, 1.0f);
|
||||
[image drawInRect:CGRectMake(0, 0, resizedSize.width, resizedSize.height) blendMode:kCGBlendModeCopy alpha:1.0f];
|
||||
image = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
|
||||
if (skipImageTransform) {
|
||||
imageSize = CGSizeMake(image.size.width * fittedOriginalSize.width / rect.size.width, image.size.height * fittedOriginalSize.height / rect.size.height);
|
||||
} else {
|
||||
imageSize = image.size;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
image = paintingImage;
|
||||
imageSize = image.size;
|
||||
}
|
||||
|
||||
if (skipImageTransform) {
|
||||
[image drawInRect:CGRectMake(0.0, 0.0, outputImageSize.width, outputImageSize.height)];
|
||||
}
|
||||
|
||||
CGSize scales = CGSizeMake(fittedImageSize.width / rect.size.width, fittedImageSize.height / rect.size.height);
|
||||
CGSize rotatedContentSize = TGRotatedContentSize(paintingImage.size, rotation);
|
||||
CGAffineTransform transform = CGAffineTransformIdentity;
|
||||
transform = CGAffineTransformTranslate(transform, outputImageSize.width / 2, outputImageSize.height / 2);
|
||||
transform = CGAffineTransformRotate(transform, TGRotationForOrientation(orientation));
|
||||
transform = CGAffineTransformTranslate(transform, (rotatedContentSize.width / 2 - CGRectGetMidX(rect)) * scales.width, (rotatedContentSize.height / 2 - CGRectGetMidY(rect)) * scales.height);
|
||||
transform = CGAffineTransformRotate(transform, rotation);
|
||||
CGContextConcatCTM(context, transform);
|
||||
|
||||
if (mirrored)
|
||||
CGContextScaleCTM(context, -1.0f, 1.0f);
|
||||
|
||||
if (!skipImageTransform) {
|
||||
[image drawAtPoint:CGPointMake(-image.size.width / 2, -image.size.height / 2)];
|
||||
}
|
||||
|
||||
if (paintingImage != nil)
|
||||
{
|
||||
if (mirrored)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@
|
|||
@property (nonatomic, copy) void (^entityRemoved)(TGPhotoPaintEntityView *);
|
||||
|
||||
- (void)updateVisibility:(bool)visible;
|
||||
- (void)seekTo:(double)timestamp;
|
||||
- (void)play;
|
||||
- (void)pause;
|
||||
- (void)resetToStart;
|
||||
|
||||
- (UIColor *)colorAtPoint:(CGPoint)point;
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,55 @@
|
|||
}
|
||||
}
|
||||
|
||||
- (void)seekTo:(double)timestamp {
|
||||
for (TGPhotoPaintEntityView *view in self.subviews)
|
||||
{
|
||||
if (![view isKindOfClass:[TGPhotoPaintEntityView class]])
|
||||
continue;
|
||||
|
||||
if ([view isKindOfClass:[TGPhotoStickerEntityView class]]) {
|
||||
[(TGPhotoStickerEntityView *)view seekTo:timestamp];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)play {
|
||||
for (TGPhotoPaintEntityView *view in self.subviews)
|
||||
{
|
||||
if (![view isKindOfClass:[TGPhotoPaintEntityView class]])
|
||||
continue;
|
||||
|
||||
if ([view isKindOfClass:[TGPhotoStickerEntityView class]]) {
|
||||
[(TGPhotoStickerEntityView *)view play];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)pause {
|
||||
for (TGPhotoPaintEntityView *view in self.subviews)
|
||||
{
|
||||
if (![view isKindOfClass:[TGPhotoPaintEntityView class]])
|
||||
continue;
|
||||
|
||||
if ([view isKindOfClass:[TGPhotoStickerEntityView class]]) {
|
||||
[(TGPhotoStickerEntityView *)view pause];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)resetToStart {
|
||||
for (TGPhotoPaintEntityView *view in self.subviews)
|
||||
{
|
||||
if (![view isKindOfClass:[TGPhotoPaintEntityView class]])
|
||||
continue;
|
||||
|
||||
if ([view isKindOfClass:[TGPhotoStickerEntityView class]]) {
|
||||
[(TGPhotoStickerEntityView *)view resetToStart];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)__unused gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)__unused otherGestureRecognizer
|
||||
{
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ const CGFloat TGPhotoPaintStickerKeyboardSize = 260.0f;
|
|||
|
||||
bool _appeared;
|
||||
bool _skipEntitiesSetup;
|
||||
bool _entitiesReady;
|
||||
|
||||
TGPhotoPaintFont *_selectedTextFont;
|
||||
TGPhotoPaintTextEntityStyle _selectedTextStyle;
|
||||
|
|
@ -268,7 +269,9 @@ const CGFloat TGPhotoPaintStickerKeyboardSize = 260.0f;
|
|||
|
||||
[strongSelf updateSettingsButton];
|
||||
};
|
||||
[_contentWrapperView addSubview:_entitiesContainerView];
|
||||
if (!_skipEntitiesSetup) {
|
||||
[_contentWrapperView addSubview:_entitiesContainerView];
|
||||
}
|
||||
_undoManager.entitiesContainer = _entitiesContainerView;
|
||||
|
||||
_dimView = [[UIView alloc] init];
|
||||
|
|
@ -1193,9 +1196,41 @@ const CGFloat TGPhotoPaintStickerKeyboardSize = 260.0f;
|
|||
TGPhotoPaintStickerEntity *entity = [[TGPhotoPaintStickerEntity alloc] initWithDocument:document baseSize:[self _stickerBaseSizeForCurrentPainting] animated:animated];
|
||||
[self _setStickerEntityPosition:entity];
|
||||
|
||||
bool hasStickers = false;
|
||||
for (TGPhotoPaintEntityView *view in _entitiesContainerView.subviews) {
|
||||
if ([view isKindOfClass:[TGPhotoStickerEntityView class]]) {
|
||||
hasStickers = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
TGPhotoStickerEntityView *stickerView = (TGPhotoStickerEntityView *)[_entitiesContainerView createEntityViewWithEntity:entity];
|
||||
[self _commonEntityViewSetup:stickerView];
|
||||
|
||||
__weak TGPhotoPaintController *weakSelf = self;
|
||||
__weak TGPhotoStickerEntityView *weakStickerView = stickerView;
|
||||
stickerView.started = ^(double duration) {
|
||||
__strong TGPhotoPaintController *strongSelf = weakSelf;
|
||||
if (strongSelf != nil) {
|
||||
TGPhotoEditorController *editorController = (TGPhotoEditorController *)self.parentViewController;
|
||||
if (![editorController isKindOfClass:[TGPhotoEditorController class]])
|
||||
return;
|
||||
|
||||
if (!hasStickers) {
|
||||
[editorController setMinimalVideoDuration:duration];
|
||||
}
|
||||
|
||||
NSTimeInterval currentTime = editorController.currentTime;
|
||||
__strong TGPhotoStickerEntityView *strongStickerView = weakStickerView;
|
||||
if (strongStickerView != nil) {
|
||||
if (!isnan(currentTime)) {
|
||||
[strongStickerView seekTo:currentTime];
|
||||
[strongStickerView play];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
[self selectEntityView:stickerView];
|
||||
_entitySelectionView.alpha = 0.0f;
|
||||
|
||||
|
|
@ -1816,7 +1851,6 @@ const CGFloat TGPhotoPaintStickerKeyboardSize = 260.0f;
|
|||
|
||||
if (self.presentedForAvatarCreation) {
|
||||
_canvasView.hidden = true;
|
||||
_entitiesContainerView.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1865,7 +1899,7 @@ const CGFloat TGPhotoPaintStickerKeyboardSize = 260.0f;
|
|||
|
||||
[self setupCanvas];
|
||||
_entitiesContainerView.hidden = false;
|
||||
|
||||
|
||||
TGPhotoEditorPreviewView *previewView = _previewView;
|
||||
[previewView setPaintingHidden:true];
|
||||
previewView.hidden = false;
|
||||
|
|
@ -1887,8 +1921,10 @@ const CGFloat TGPhotoPaintStickerKeyboardSize = 260.0f;
|
|||
CGPoint boundsCenter = TGPaintCenterOfRect(_contentWrapperView.bounds);
|
||||
_entitiesContainerView.center = TGPaintAddPoints(boundsCenter, offset);
|
||||
|
||||
[_contentWrapperView addSubview:_entitiesContainerView];
|
||||
|
||||
if (!_skipEntitiesSetup || _entitiesReady) {
|
||||
[_contentWrapperView addSubview:_entitiesContainerView];
|
||||
}
|
||||
_entitiesReady = true;
|
||||
[self resetScrollView];
|
||||
}
|
||||
|
||||
|
|
@ -2322,6 +2358,13 @@ const CGFloat TGPhotoPaintStickerKeyboardSize = 260.0f;
|
|||
|
||||
previewView.frame = previewFrame;
|
||||
|
||||
if ([self presentedForAvatarCreation]) {
|
||||
CGAffineTransform transform = CGAffineTransformMakeRotation(TGRotationForOrientation(photoEditor.cropOrientation));
|
||||
if (photoEditor.cropMirrored)
|
||||
transform = CGAffineTransformScale(transform, -1.0f, 1.0f);
|
||||
previewView.transform = transform;
|
||||
}
|
||||
|
||||
CGSize fittedOriginalSize = CGSizeMake(originalSize.width * ratio, originalSize.height * ratio);
|
||||
CGSize rotatedSize = TGRotatedContentSize(fittedOriginalSize, rotation);
|
||||
CGPoint centerPoint = CGPointMake(rotatedSize.width / 2.0f, rotatedSize.height / 2.0f);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@
|
|||
|
||||
@interface TGPhotoStickerEntityView : TGPhotoPaintEntityView
|
||||
|
||||
@property (nonatomic, copy) void(^started)(double);
|
||||
|
||||
@property (nonatomic, readonly) TGPhotoPaintStickerEntity *entity;
|
||||
@property (nonatomic, readonly) bool isMirrored;
|
||||
|
||||
|
|
@ -17,6 +19,10 @@
|
|||
- (UIImage *)image;
|
||||
|
||||
- (void)updateVisibility:(bool)visible;
|
||||
- (void)seekTo:(double)timestamp;
|
||||
- (void)play;
|
||||
- (void)pause;
|
||||
- (void)resetToStart;
|
||||
|
||||
- (CGRect)realBounds;
|
||||
|
||||
|
|
|
|||
|
|
@ -55,6 +55,13 @@ const CGFloat TGPhotoStickerSelectionViewHandleSide = 30.0f;
|
|||
_mirrored = entity.isMirrored;
|
||||
|
||||
_stickerView = [context stickerViewForDocument:entity.document];
|
||||
|
||||
__weak TGPhotoStickerEntityView *weakSelf = self;
|
||||
_stickerView.started = ^(double duration) {
|
||||
__strong TGPhotoStickerEntityView *strongSelf = weakSelf;
|
||||
if (strongSelf != nil && strongSelf.started != nil)
|
||||
strongSelf.started(duration);
|
||||
};
|
||||
[self addSubview:_stickerView];
|
||||
|
||||
_document = entity.document;
|
||||
|
|
@ -178,6 +185,22 @@ const CGFloat TGPhotoStickerSelectionViewHandleSide = 30.0f;
|
|||
[_stickerView setIsVisible:visible];
|
||||
}
|
||||
|
||||
- (void)seekTo:(double)timestamp {
|
||||
[_stickerView seekTo:timestamp];
|
||||
}
|
||||
|
||||
- (void)play {
|
||||
[_stickerView play];
|
||||
}
|
||||
|
||||
- (void)pause {
|
||||
[_stickerView pause];
|
||||
}
|
||||
|
||||
- (void)resetToStart {
|
||||
[_stickerView resetToStart];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue