Merge branch 'custom-webrtc-build' into experimental-2

This commit is contained in:
Ali 2020-08-14 13:09:59 +01:00
commit faf66c9b4b
837 changed files with 180761 additions and 7926 deletions

View file

@ -37,4 +37,4 @@ Remove, if not applicable
**iOS version**: `13.X`
**App version:** `5.X`
**App version:** `7.X`

View file

@ -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
View file

@ -0,0 +1 @@
LS3VfNVetXNy6mHmek8hegOh0wsvmJs0hcrc7PLA9eI=

View file

@ -3,7 +3,7 @@
@implementation Serialization
- (NSUInteger)currentLayer {
return 116;
return 117;
}
- (id _Nullable)parseMessage:(NSData * _Nullable)data {

View file

@ -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.";

View file

@ -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.";

View file

@ -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.";

View file

@ -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.";

View 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&apos;ll be there in 10 minutes</string>
</array>
</dict>
</array>
</dict>
</plist>

View file

@ -0,0 +1,12 @@
/* Localized versions of Info.plist keys */
"NSContactsUsageDescription" = "تلگرام به طور مداوم مخاطبین شما را به سرورهای اَبری و رمزگذاری شده خود آپلود می‌کند تا شما بتوانید در تمامی دستگاه‌های خود با دوستانتان ارتباط برقرار کنید.";
"NSLocationWhenInUseUsageDescription" = "هنگامی که شما موقعیت مکانی خود را برای دوستانتان ارسال می‌کنید، تلگرام جهت نشان دادن نقشه به آن‌ها نیازمند دسترسی است.";
"NSLocationAlwaysAndWhenInUseUsageDescription" = "در صورتی که تصمیم بگیرید موقعیت مکانی لحظه‌ای خود را در یک گفتگو با دوستانتان به اشتراک بگذارید، تلگرام جهت به‌روزرسانی این اطلاعات در طول به اشتراک گذاری لحظه‌ای، باید در پس‌زمینه به موقعیت مکانی شما دسترسی داشته باشد.";
"NSLocationAlwaysUsageDescription" = "در صورتی که شما تصمیم بگیرید موقعیت مکانی لحظه‌ای خود را در یک گفتگو با دوستانتان به اشتراک بگذارید، تلگرام جهت به‌روزرسانی این اطلاعات در طول به اشتراک گذاری لحظه‌ای، باید در پس‌زمینه به موقعیت مکانی شما دسترسی داشته باشد. شما همچنین برای ارسال موقعیت مکانی از ساعت اَپل به این دسترسی نیاز دارید.";
"NSCameraUsageDescription" = "ما به این دسترسی نیاز داریم تا شما بتوانید عکس‌ و ویدیو گرفته و آن‌ها را به اشتراک بگذارید.";
"NSPhotoLibraryUsageDescription" = "ما به این دسترسی نیاز داریم تا شما بتوانید از گالری تصاویر خود عکس و ویدیو به اشتراک بگذارید.";
"NSPhotoLibraryAddUsageDescription" = "ما به این دسترسی نیاز داریم تا شما بتوانید عکس‌ها و ویدیوها را در گالری تصاویر خود ذخیره کنید.";
"NSMicrophoneUsageDescription" = "ما به این دسترسی نیاز داریم تا شما بتوانید پیام‌های صوتی و ویدیوهای صدادار را ضبط کرده و به اشتراک بگذارید.";
"NSSiriUsageDescription" = "شما می‌توانید با استفاده از Siri پیام ارسال کنید.";
"NSFaceIDUsageDescription" = "شما می‌توانید با استفاده‌ از Face ID قفل برنامه را باز کنید.";

View 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.";

View file

@ -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.";

View 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&apos;ll be there in 10 minutes</string>
</array>
</dict>
</array>
</dict>
</plist>

View 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ę.";

View 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.";

View 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&apos;ll be there in 10 minutes</string>
</array>
</dict>
</array>
</dict>
</plist>

View 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.";

View file

@ -1 +0,0 @@
/private/var/tmp/_bazel_ilya/b5422c0ab62ebf818a9d47ff552063a2/execroot/__main__

View file

@ -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"

View file

@ -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?

View file

@ -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)
}

View file

@ -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 }

View file

@ -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) {

View file

@ -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
}

View file

@ -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
}
}
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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
}

View file

@ -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

View file

@ -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?()

View file

@ -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)
})))

View file

@ -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: [

View file

@ -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)?

View file

@ -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
}

View file

@ -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
}
}

View file

@ -1028,7 +1028,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
}
}, setItemPinned: { _, _ in
}, setPeerMuted: { _, _ in
}, deletePeer: { _ in
}, deletePeer: { _, _ in
}, updatePeerGrouping: { _, _ in
}, togglePeerMarkedUnread: { _, _ in
}, toggleArchivedFolderHiddenByDefault: {

View file

@ -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

View file

@ -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
}

View file

@ -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

View file

@ -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))
}
})
})*/
}
}
}

View file

@ -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

View file

@ -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)
}

View file

@ -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)

View file

@ -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)

View file

@ -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) {

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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 {

View file

@ -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

View file

@ -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()

View file

@ -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))
}
}

View file

@ -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) {

View file

@ -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()

View file

@ -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

View file

@ -66,7 +66,7 @@ public final class HashtagSearchController: TelegramBaseController {
}, setPeerIdWithRevealedOptions: { _, _ in
}, setItemPinned: { _, _ in
}, setPeerMuted: { _, _ in
}, deletePeer: { _ in
}, deletePeer: { _, _ in
}, updatePeerGrouping: { _, _ in
}, togglePeerMarkedUnread: { _, _ in
}, toggleArchivedFolderHiddenByDefault: {

View file

@ -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",

View file

@ -4,8 +4,11 @@ swift_library(
name = "ImageCompression",
module_name = "ImageCompression",
srcs = glob([
"Sources/**/*.swift",
"Sources/**/*.swift",
]),
deps = [
"//submodules/MozjpegBinding:MozjpegBinding",
],
visibility = [
"//visibility:public",
],

View file

@ -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)
}

View file

@ -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 {

View file

@ -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)

View file

@ -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)
}
}

View file

@ -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

View file

@ -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>

View file

@ -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

View file

@ -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

View file

@ -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;

View file

@ -85,4 +85,7 @@ typedef enum {
+ (TGPhotoEditorTab)defaultTabsForAvatarIntent;
- (NSTimeInterval)currentTime;
- (void)setMinimalVideoDuration:(NSTimeInterval)duration;
@end

View file

@ -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;

View file

@ -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);

View file

@ -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;

View file

@ -13,6 +13,7 @@ typedef enum
TGMediaVideoConversionPresetCompressedVeryHigh,
TGMediaVideoConversionPresetAnimation,
TGMediaVideoConversionPresetVideoMessage,
TGMediaVideoConversionPresetProfileLow,
TGMediaVideoConversionPresetProfile,
TGMediaVideoConversionPresetProfileHigh,
TGMediaVideoConversionPresetProfileVeryHigh,

View file

@ -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

View file

@ -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

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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) {

View file

@ -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;

View file

@ -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

View file

@ -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));

View file

@ -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;

View file

@ -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)

View file

@ -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];
}

View file

@ -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];
}

View file

@ -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();

View file

@ -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];
}];
}

View file

@ -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)

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -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;

View file

@ -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